# Test mode and magic numbers

Source: https://docs.usetone.ai/test-mode

> 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:

```bash
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

```bash
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`:

```json
{
  "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:

```bash
curl "$TONE_API/v1/compliance/check" \
  -X POST -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"e164":"+915555000005"}'
```

```json
{
  "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:

```json
{
  "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.
