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_ | |
|---|---|---|
| Carrier | Simulated | Real |
| Phones | Nothing rings | A real person's phone rings |
| Money | Priced, never charged | Charged to your wallet |
| Call lifecycle | Complete — states, dispositions, webhooks, CDR rows | Complete |
| Compliance gate | Runs, with the same rules | Runs |
| Available | From signup, before KYC | Keys 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".
| Number | What happens |
|---|---|
+91 55550 00001 | Answered. 45-second call, with a canned summary and structured outputs your assertions can match on |
+91 55550 00002 | Busy |
+91 55550 00003 | No answer |
+91 55550 00004 | Voicemail |
+91 55550 00005 | On 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 00006 | Answered, 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/outputson...0001are 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 inpeer, exactly as the live call it stands in for does, and it still billsbilledPaise: 0. - Sandbox inventory is not carrier inventory. The specific
e164values 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 withenvironment_mismatch, and a live resource in a URL path answers404. 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
environmentsays 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
summaryoroutputs, 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.