Skip to content

Test mode (sandbox)

Test mode is the Configure sandbox: a self-serve Configure account with a user already in it. One unauthenticated POST gives you a test key pair and a synthetic user whose profile is already full, so your first read comes back personalized. No signup, no dashboard, no phone of your own.

All docs pages, indexed for agents: https://docs.configure.dev/llms.txt (add .md to any page URL for plain markdown).

bash
curl -X POST https://api.configure.dev/v1/sandbox/provision
json
{
  "ok": true,
  "claim_status": "unclaimed",
  "expires_in_days": 30,
  "agent": "sandbox-96d4069b",
  "agent_display_name": "Sandbox Agent",
  "secret_key": "sk_test_...",
  "publishable_key": "pk_test_...",
  "sandbox_user": {
    "user_id": "1528c26b-7fd0-42f7-9dd9-2b27b8ed582a",
    "external_id": "sandbox-user",
    "name": "Nova Sandbrook",
    "test_phone": "+12525550137",
    "otp_code": "424242"
  },
  "livemode": false
}

Save secret_key now. It is hashed at rest and never shown again. The endpoint is rate limited per IP, so script one provision per environment rather than one per test.

bash
CONFIGURE_API_KEY=sk_test_...          # server only, like any secret key
CONFIGURE_PUBLISHABLE_KEY=pk_test_...  # safe in the browser
CONFIGURE_AGENT=sandbox-96d4069b       # the agent the sandbox created for you
CONFIGURE_TEST_PHONE=+12525550137      # sandbox_user.test_phone, minted per sandbox

Those four values come out of your own provision response, the phone number included: test_phone is minted per sandbox, so the one printed here resolves for nobody else. With them exported, every command below runs as written.

Every REST and SDK call on this site works unchanged with these values. The key prefix is the only thing that selects test mode: same host, same routes, same request shapes, same errors. The one carve-out is browser sign-in tooling: a sandbox mints no OAuth client, so npx configure verify proves your test keys and marks the OAuth checks "skip (sandbox: no OAuth client)". Skips there are expected, not a broken sandbox.

Read the synthetic user

Nova Sandbrook is an unlinked user under your sandbox developer account, so a secret key plus X-User-Id reads her:

bash
curl "https://api.configure.dev/v1/profile" \
  -H "X-API-Key: $CONFIGURE_API_KEY" \
  -H "X-Agent: $CONFIGURE_AGENT" \
  -H "X-User-Id: sandbox-user"
json
{
  "linked": true,
  "identity": {
    "name": "Nova Sandbrook",
    "email": "nova.sandbrook@example.com",
    "occupation": "Freelance industrial designer",
    "location": "Portland, Oregon",
    "sandbox": true
  },
  "preferences": [
    "Communication: direct and concise; bullet points over prose",
    "Schedule: deep work before noon; no meetings on Thursdays (boatyard day)",
    "Diet: vegetarian; shellfish allergy (severe); oat-milk flat whites"
  ],
  "livemode": false
}

The JSON above is an excerpt; the full response is much larger. The fixture is a real profile, written through the same storage the live plane uses: identity, preferences, a summary, typed memories grouped into boxes, imported ChatGPT memories, and connected Gmail and Calendar accounts with canned mail and events. That is the point of the sandbox. An empty account cannot show you what a personalized first message reads like.

livemode

Every JSON object a test key produces carries livemode: false, including error bodies. Live responses are untouched, so the field is how a client tells the two planes apart without inspecting keys:

json
{ "error": { "code": "tool_not_connected" }, "request_id": "req_...", "livemode": false }

Test and live data cannot see each other. A test key resolving a live user, or a live key resolving a test user, does not resolve at all: the user is not found rather than forbidden.

Sign in as the synthetic user

The test phone is in the fictional +1 NXX 555-01XX range that can never receive a message, and its OTP is fixed at 424242. Under a test key that code verifies with no provider round trip. Under a live key these numbers get no special treatment at all: the code goes to the SMS provider and fails like any wrong code.

Use it to drive your own sign-in UI end to end. The hosted components and the OTP endpoints both accept it:

bash
curl -X POST https://api.configure.dev/v1/auth/otp/start \
  -H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT" \
  -H 'Content-Type: application/json' \
  -d "{\"phone\":\"$CONFIGURE_TEST_PHONE\"}"

curl -X POST https://api.configure.dev/v1/auth/otp/verify \
  -H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT" \
  -H 'Content-Type: application/json' \
  -d "{\"phone\":\"$CONFIGURE_TEST_PHONE\",\"code\":\"424242\",\"agent\":\"$CONFIGURE_AGENT\"}"

That returns a user token, which is what a browser sign-in produces. Server-side calls want an agent token instead, and the synthetic user has already approved the sandbox agent, so phone recognition hands you one in a single call:

bash
curl -X POST https://api.configure.dev/v1/auth/sign-in/recognize-phone \
  -H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT" \
  -H 'Content-Type: application/json' \
  -d "{\"candidates\":[\"$CONFIGURE_TEST_PHONE\"]}"
json
{
  "matched": true, "approved": true, "linked": true,
  "userId": "1528c26b-7fd0-42f7-9dd9-2b27b8ed582a",
  "agent": "sandbox-96d4069b",
  "token": "eyJhbGciOiJIUzI1...",
  "displayName": "Nova Sandbrook",
  "livemode": false
}

You need that token for connector calls, so keep it: pipe the call above through | jq -r .token and assign it, TOKEN=$(curl -sS ... | jq -r .token). X-User-Id developer scope covers profile read, search, and remember; connector query endpoints require an agent-scoped token, in test mode exactly as in live.

bash
curl -X POST https://api.configure.dev/v1/connectors/gmail/messages/search \
  -H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"query":"harbor","max_results":2}'

The mail comes back from the fixture: a design review from her anchor client, a shipping notice from a chandlery. No provider is contacted.

Break it on purpose, then put it back

Two endpoints, both authenticated with your sk_test_ key. They exist so you can write the unhappy path before a real user ever hits it.

bash
# Make Gmail behave like a connection whose token died.
curl -X POST https://api.configure.dev/v1/sandbox/simulate \
  -H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT" \
  -H 'Content-Type: application/json' \
  -d '{"scenario":"gmail_token_expired"}'

Gmail calls on this sandbox now fail the way a dead Composio connected account fails: HTTP 400, tool_not_connected, retryable: false. Handling that failure is the subject of connector repair. Calendar and profile reads are unaffected.

bash
# Restore the synthetic user to pristine: test-plane writes deleted, fixture reseeded.
curl -X POST https://api.configure.dev/v1/sandbox/reset \
  -H "X-API-Key: $CONFIGURE_API_KEY" -H "X-Agent: $CONFIGURE_AGENT"

Reset also clears the simulated scenario, so a test suite can reset between cases and start from the same profile every time.

gmail_token_expired is the only scenario today.

Teach your agent the tool pattern here

The sandbox is where a coding agent should learn the Configure tool loop: every step is rehearsable and reversible. The loop, the same one in tool calling, is:

  1. Advertise everything: pass [...yourTools, ...profile.tools()] to the model. Do not pre-filter by what is connected.
  2. Route by prefix: any configure_* call goes to profile.executeTool(call); everything else is yours.
  3. A refusal is an answer, not a crash. When a call fails, read error.suggestedAction: connect_tool and reconnect mean "give the user this connect link", not "retry" (connector repair). Rehearse exactly this with POST /v1/sandbox/simulate: break Gmail, watch your branch produce the link, reset, run it again.
  4. Personalize from the read: the first configure_profile_read should change your agent's first answer (Nova's profile is rich enough to show it).
  5. Commit after the turn: close the loop with a write-back once the reply is out. The sandbox is the one place write-back is freely reversible: POST /v1/sandbox/reset deletes test-plane writes and reseeds the fixture, so commit as much as you like and start every run from the same profile.

An agent connected over MCP instead of the SDK gets the same tools natively from https://mcp.configure.dev, with the same names and the same refusal-with-connect-link behavior. What your agent learns against the sandbox transfers unchanged (the MCP adapter maps the two surfaces tool by tool). Today the sandbox itself speaks the SDK/REST plane; sign-in at mcp.configure.dev is always as a real user.

Lifetime and going live

An unclaimed sandbox expires 30 days after it is provisioned, and everything it created is removed with it. Provision another whenever you need one.

Nothing carries over to a live account, by design: test data never appears on the live plane. When you are ready for real users, get live credentials the ordinary way (Get your credentials) and change the two keys in your environment. No code changes, because nothing on the request side differs.

Do not point a sandbox at a real user. The test plane is for the synthetic user, and the fictional phone range means no one can be messaged by accident.

Personalization infrastructure for agents