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 | 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 | Answered, and the number reads as listed on the carrier DND registry — a compliance warning, not a block |
+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".
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 warns, it does not block. Carrier DND data is advisory in Tone's gate: it flags a
call, it never refuses one. Ask for a verdict directly and you can see it:
curl "$TONE_API/v1/compliance/check" \
-X POST -H "Authorization: Bearer $TONE_KEY" \
-H 'content-type: application/json' \
-d '{"e164":"+915555000005"}'{
"data": {
"e164": "+915555000005",
"purpose": "service",
"allowed": true,
"blockedBy": null,
"checks": [ { "checkType": "carrier_dnd", "outcome": "warn", "...": "..." } ]
}
}allowed: true with a warn is the correct outcome, and your code should treat it as one.
...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. - Calls and numbers are environment-scoped; agents and knowledge bases are not. A
tone_test_key naming a live number — or the reverse — fails withenvironment_mismatch, and a test key structurally cannot read live calls. 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.