Tone docs

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 KYCAfter 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 00005Answered, and the number reads as listed on the carrier DND registry — a compliance warning, not a block
+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".

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/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.
  • Calls and numbers are environment-scoped; agents and knowledge bases are not. A tone_test_ key naming a live number — or the reverse — fails with environment_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.
Was this page helpful?

On this page