Tone docs

Quickstart

Build a voice agent, give it a phone number, and place a call with a known outcome — in five minutes, for free.

By the end of this page you'll have placed a call through the Tone API and read back its outcome. It costs nothing, no phone rings, and you don't need KYC, a DLT registration, or a wallet balance — everything here runs in test mode.

Everything you build here works unchanged in production. Going live is a key swap.

1. Get a test key

In the dashboard, open Developer → API keys and create a key with environment test and scope write. Copy it — it's shown once.

export TONE_API=https://apibeta.usetone.ai
export TONE_KEY=tone_test_...

Check it works:

curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/limits"

You should get your request budget and concurrency quotas back. If you get a 401, the key is wrong or revoked; if you get insufficient_scope later on, it was minted read-only.

2. Create an agent

An agent is the thing that talks: a prompt, a voice, and a language.

curl "$TONE_API/v1/agents" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "name": "Order confirmation",
    "purpose": "transactional",
    "systemPrompt": "You call customers to confirm a cash-on-delivery order. Confirm the order, ask which day suits them for delivery, then thank them and end the call. Be brief and polite.",
    "voice": {
      "ttsModel": "bulbul:v3",
      "ttsVoice": "simran",
      "sttModel": "saaras:v3",
      "languages": ["hi-IN", "en-IN"]
    }
  }'
{ "data": { "id": "a4f21c8e-...", "name": "Order confirmation", "status": "draft", "...": "..." } }

Save the id as $AGENT.

Two fields deserve a moment:

purpose is your call's regulatory classification — transactional, service, promotional, or collections — and it is not cosmetic. India's TCCCPR rules restrict when promotional and collections calls may be placed, and Tone's compliance gate enforces that on every dial using this field. There is deliberately no way to override it per call; if there were, the audit trail would be worthless. Classify honestly.

voice.ttsVoice must be valid for ttsModel. Voices aren't portable across model versions: simran is a bulbul:v3 voice and is rejected on bulbul:v2. A bad pairing is a 400 at write time rather than a failure mid-call. Browse what's available:

curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/catalog/voice"

Publish it

Agents are created as drafts:

curl "$TONE_API/v1/agents/$AGENT" \
  -X PATCH \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"status":"live"}'

You can dial with a draft agent — outbound calls run whatever you point them at, which is what makes iterating quick. Publishing matters for inbound: a number whose agent is still a draft refuses incoming calls, on the grounds that a half-written prompt shouldn't answer a real customer.

3. Get a phone number

In test mode, numbers are free and allocated on request:

curl "$TONE_API/v1/numbers" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"series":"regular","agentId":"'"$AGENT"'"}'
{ "data": { "id": "b91e...", "e164": "+918041000123", "status": "active", "environment": "test", "agentId": "a4f21c8e-...", "...": "..." } }

Save the id as $NUMBER. Binding agentId here is what makes this agent answer calls to this number; for placing calls, you'll name the agent explicitly.

In production you'd search real carrier inventory and buy — see Going live.

4. Place a call

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",
    "variables": { "order_id": "SO-4417", "customer_name": "Priya" }
  }'
{ "data": { "id": "3b7e...", "status": "queued", "direction": "outbound", "environment": "test", "...": "..." } }

Three things happened in that request:

  • +915555000001 is a magic number — in test mode it always answers, for 45 seconds, with a canned summary. ...0002 is always busy, ...0003 never answers. Deterministic outcomes are what let you write real tests.
  • Idempotency-Key makes the request safe to retry. If the connection drops and you send it again with the same key, you get the original call back rather than dialling twice. More on idempotency.
  • variables fill the {{placeholders}} in your agent's prompt, per call. They're stored on the call record and returned by reads.

If you get a 403

{ "error": { "type": "compliance", "code": "blocked_dnd", "message": "That number is on your Do-Not-Call list." } }

That's the compliance gate, and it runs in test mode exactly as it does live. Handle it as a normal outcome, not an exception: some fraction of any real recipient list is going to come back blocked. See the compliance guide.

5. Read the outcome

The sandbox rings, then settles, within a few seconds:

curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/calls/3b7e..."
{
  "data": {
    "id": "3b7e...",
    "status": "ended",
    "disposition": "answered",
    "durationSeconds": 45,
    "billedPaise": 0,
    "summary": "Sandbox call: the customer confirmed the order and asked for delivery on Thursday.",
    "outputs": { "confirmed": true, "preferred_day": "thursday", "sandbox": true },
    "variables": { "order_id": "SO-4417", "customer_name": "Priya" }
  }
}

status moves queuedin_progressended. Only when it reaches ended is disposition meaningful — answered, no_answer, busy, voicemail, failed, or unknown. Treat that list as open: new dispositions can appear without being a breaking change.

outputs is structured data extracted from the conversation, shaped by the output variables you declare on the agent. That's usually the field your application actually cares about — the transcript is for humans, outputs is for your database.

6. Stop polling — use webhooks

Polling works for a quickstart. For anything real, subscribe:

curl "$TONE_API/v1/integrations/webhooks" \
  -X POST \
  -H "Authorization: Bearer $TONE_ADMIN_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://your-app.example.com/hooks/tone",
    "events": ["call.completed", "call.failed"]
  }'

The response contains a whsec_... signing secret, shown once. Every delivery is signed with it, and verifying that signature is not optional — see Webhooks for the full catalog, the verification snippet, and the retry behaviour.

Registering an endpoint needs an admin key, because the URL decides where your call data flows.

7. Going live

export TONE_KEY=tone_live_...

That is genuinely the code change. What has to be true around it:

  1. Complete KYC and DLT registration in the dashboard. Live keys unlock at verification, and the compliance gate blocks promotional traffic without an approved DLT PE ID.

  2. Fund your wallet. Live calls debit per minute; live numbers charge a setup fee and a monthly rental. Subscribe to wallet.balance.low so you hear about it before a campaign pauses itself.

  3. Buy a real number with an admin key. Search inventory, then purchase — and note the mandatory Idempotency-Key, because a duplicate purchase is a second monthly rental:

    curl -H "Authorization: Bearer $TONE_LIVE_ADMIN_KEY" \
      "$TONE_API/v1/numbers/available?number_type=mobile&region=KA"
    
    curl "$TONE_API/v1/numbers/purchase" \
      -X POST \
      -H "Authorization: Bearer $TONE_LIVE_ADMIN_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H 'content-type: application/json' \
      -d '{"e164":"+918041234567","label":"orders line"}'
  4. Register a live webhook endpoint. Endpoints and their secrets are per-endpoint, not shared between environments — point test and live at different URLs so a sandbox event can never be mistaken for a real one.

  5. Re-check your error handling against the error catalog, especially insufficient_funds, concurrent_call_limit_reached, and the blocked_* family. In test mode you had no wallet and no real capacity limits; in production both bite.

Where to go next

  • Calling many people — create a campaign, add recipients, launch it. Campaigns handle pacing, concurrency, retries, and the calling-window rules for you.
  • Bring your own voice stack — keep Tone's numbers and compliance, run your own AI.
  • Compliance as an API — verdicts, consent, and suppression, with or without placing calls through us.
  • Knowledge bases — upload documents your agent can answer from, and attach them with knowledgeBaseIds on the agent.
Was this page helpful?

On this page