Tonedocs

Test mode and magic numbers

A full sandbox with deterministic call outcomes, so your integration has real tests instead of hopeful ones.

Every Tone account has two parallel universes, chosen by which key you send:

tone_test_tone_live_
CarrierSimulatedReal
PhonesNothing ringsA real person's phone rings
MoneyPriced, never chargedCharged to your wallet
Call lifecycleComplete — states, dispositions, webhooks, CDR rowsComplete
Compliance gateRuns, with the same rulesRuns
AvailableFrom signup, before KYCKeys from signup; buying numbers, live campaigns and SIP trunks after verification

Test mode isn't a stub. Calls move through the same states, settle with the same dispositions, emit the same webhooks with the same signatures, and land in the same call log with the same fields. Going live is a key swap and nothing else.

That includes the compliance gate: a call your live integration would be blocked from placing is blocked in test too, with the same 403 and the same audit trail. Discovering a compliance problem in your sandbox is the point.

Getting a sandbox number for free

Test numbers don't have to be bought. POST /v1/numbers allocates one from the sandbox pool — no carrier, no wallet, write scope:

curl "$TONE_API/v1/numbers" \
  -X POST -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"series":"regular","agentId":"YOUR_AGENT_ID"}'

POST /v1/numbers/purchase — which names a specific e164 and goes to a carrier — also works with a test key against simulated inventory, if what you're testing is the purchase flow. For everything else, assign.

Magic numbers

Dial one of these from a test-mode call and the outcome is fixed. Same number, same disposition, same webhook, every time — which is what makes an integration testable in CI instead of "we called ourselves and it seemed fine".

NumberWhat happens
+91 55550 00001Answered. 45-second call, with a canned summary and structured outputs your assertions can match on
+91 55550 00002Busy
+91 55550 00003No answer
+91 55550 00004Voicemail
+91 55550 00005On India's DND register. A promotional call to it is blocked (blocked_dnd); a transactional or service call is answered, because DND does not apply to it
+91 55550 00006Answered, and the callee opts out mid-call. Your next dial to this number is blocked

Any other number in test mode simply answers. Write them without spaces: "toE164": "+915555000001".

On a number routed to your own endpoint (BYO), every call that answers reaches your endpoint, with a recorded caller on the line instead of a person — see Testing a BYO number. Busy, no answer and voicemail still end on their own.

Outcomes take a few seconds to land — the sandbox rings before it answers, so your call.initiated webhook reliably precedes your call.completed one, exactly as it does live.

Asserting on a known outcome

curl "$TONE_API/v1/calls" \
  -X POST -H "Authorization: Bearer $TONE_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{"agentId":"'"$AGENT"'","numberId":"'"$NUMBER"'","toE164":"+915555000001"}'

Poll GET /v1/calls/{id} (or wait for the webhook) until status is ended:

{
  "data": {
    "id": "3b7e...",
    "status": "ended",
    "disposition": "answered",
    "durationSeconds": 45,
    "summary": "Sandbox call: the customer confirmed the order and asked for delivery on Thursday.",
    "outputs": { "confirmed": true, "preferred_day": "thursday", "sandbox": true }
  }
}

outputs.confirmed === true is a stable assertion. So is disposition === "busy" for ...0002.

The two compliance magics

These are the ones worth understanding, because they behave the way the real rules behave rather than the way a mock would.

...0005 is on the DND register, and that only matters for promotional calls. A promotional call to it is refused before it is placed. Ask for a verdict directly and you can see the check:

curl "$TONE_API/v1/compliance/check" \
  -X POST -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"e164":"+915555000005","purpose":"promotional"}'
{
  "data": {
    "e164": "+915555000005",
    "purpose": "promotional",
    "allowed": false,
    "blockedBy": "dnd_register",
    "checks": [ { "checkType": "dnd_register", "outcome": "block", "reason": "dnd_listed", "...": "..." } ]
  }
}

Ask again with "purpose":"transactional" and the same check passes as not_applicable: DND preferences do not govern transactional or service calls. It is the only test number on the register; any other number reads as clear.

...0006 blocks — on the second call. The first dial answers and the callee opts out, which revokes their consent and writes a 90-day suppression to your own do-not-call list. Dial it again:

{
  "error": {
    "type": "compliance",
    "code": "blocked_dnd",
    "message": "That number is on your Do-Not-Call list.",
    "doc_url": "https://docs.usetone.ai/errors#blocked_dnd",
    "request_id": "req_8f14a2..."
  }
}

403. A compliance.check.blocked webhook fires alongside it, and the suppression is visible at GET /v1/dnc.

Those two dials are the entire opt-out lifecycle — consent revocation, suppression, refusal, webhook, audit trail — with no fixtures to set up. If your integration handles a 403 from POST /v1/calls correctly, you've tested the thing most likely to bite you in production.

What differs from live

Being honest about the edges, so nothing surprises you at cutover:

  • Nobody speaks. The sandbox produces call outcomes, not conversations. There's no audio, no transcript, and the summary/outputs on ...0001 are canned rather than generated. To hear your agent actually talk, use the test call in the dashboard, which runs the real voice pipeline through your browser.
  • Prices are simulated. A sandbox number quotes a plausible setup and rental, and a sandbox call is priced — but nothing is debited, and the numbers are not a quote. What makes a sandbox call free is the environment, nothing else: a test call reads channel: "pstn" and reports the number it dialled in peer, exactly as the live call it stands in for does, and it still bills billedPaise: 0.
  • Sandbox inventory is not carrier inventory. The specific e164 values available to search and buy in test are made up. Don't build anything that expects a particular one to exist.
  • Everything a test key reads is test data; agents and knowledge bases are shared. A tone_test_ key naming a live number in a request body — or the reverse — fails with environment_mismatch, and a live resource in a URL path answers 404. Events, compliance checks, sealed evidence, consents and suppression entries are pinned to the key's environment too. But an agent is one object in both universes: the agent you tuned against magic numbers is the same agent, with the same prompt and voice, that answers a real phone the moment you swap the key. That's deliberate — an agent you tested and an agent you shipped should not be two objects that can drift apart. The same is true of knowledge bases.
  • Webhook endpoints and the suppression list are shared; consents are not. One webhook endpoint, with one signing secret, receives both test and live events — the envelope's environment says which — so endpoints are managed with a live key or in the dashboard (a test key can list them). A do-not-call entry recorded with a test key — including the opt-out on ...0006 — protects that person from live dials as well. A consent recorded with a test key counts for test calls only.
  • A BYO number's sandbox call reaches your endpoint with a recorded caller. When the dialled number answers, Tone opens your endpoint exactly as on a live call and plays one recorded line into it, then hangs up once your bot has answered. The call carries no summary or outputs, because Tone runs no agent on it. To talk to your bot yourself, use Start test call in the number's settings in the dashboard.
Was this page helpful?

On this page