Bring your own voice stack
Keep your own STT, LLM and TTS. Rent Tone's +91 numbers, carrier and TRAI compliance, and get every call's audio over a WebSocket.
You already have a voice agent — Pipecat, LiveKit, Retell, your own pipeline — and what you're missing is an Indian phone number you can legally dial from. That's this tier.
Tone owns the number, the carrier relationship, the pre-dial compliance gate, the CDR and the billing. Every call opens a WebSocket to your server carrying the caller's audio, and accepts yours back. What the agent says is entirely yours.
₹2.50 per minute — telephony plus platform — plus the number's monthly rental. You're not paying for an AI stack you aren't using. A call that never connects, or that Tone refused, bills nothing.
The wire protocol is Twilio Media Streams-shaped: the same seven events, the same camelCase
fields, the same base64 payloads. Code written against Twilio — including Pipecat's
TwilioFrameSerializer — works here with at most a config change.
Read Tone Media Streams for the full protocol: every frame, the audio formats, SIP trunking for hosted platforms, and the local test harness. This page is the API path to a working call.
1. Declare your sender classification
There's no Tone agent on a BYO number, so there's nothing for the compliance gate to read a call purpose from. Declare it once for the organization:
This one is dashboard-only. Open Telephony → Compliance → Sender classification and set it there. An API key gets a
403, deliberately: the classification decides which calling window applies to every dial you place on your own infrastructure, so loosening it is a decision an accountable person makes rather than something a deploy script does. The change is recorded as aprofile_changeaudit row either way.
transactional, service, promotional, or collections — the Sender classification field
under Telephony → Compliance in the dashboard. This drives the calling-window rules
on every dial, so classify honestly — and note that dialling before you set it is a 422, by
design. Defaulting strict would silently window-block a support line; defaulting loose would
under-check a marketer.
2. Store your endpoint's credential
Your WebSocket server presumably wants an Authorization header. Put the value in Tone's secret
store rather than in the endpoint config — it's encrypted at rest and only decrypted on the signed
path that opens the call:
Also dashboard-only — Developer → Secrets → New secret. The store is write-only by design: there is no read-back path for a credential, and an API key cannot create one. Copy the returned secret id; that is what
mediaEndpoint.authSecretIdreferences below.
Keep the returned id. There is no read-back endpoint — this is the only moment the plaintext
crosses the API. Passing a credential inline in mediaEndpoint instead is refused outright, since
that field rides ordinary dashboard reads.
3. Point a number at your server
Get a number first — assign a free sandbox one
with a test key, or buy a real one with an admin key. Then switch its routing:
curl "$TONE_API/v1/numbers/$NUMBER" \
-X PATCH \
-H "Authorization: Bearer $TONE_KEY" \
-H 'content-type: application/json' \
-d '{
"routingMode": "byo_ws",
"mediaEndpoint": {
"type": "static",
"url": "wss://bot.example.com/media",
"format": "linear16",
"sampleRate": 8000,
"authSecretId": "SECRET_ID",
"customParameters": { "team": "support" }
}
}'Tone appends ?callId=<id> to a static URL. If your server can't carry session state in a URL, use
"type": "webhook" with an https:// URL instead: Tone POSTs {callId, from, to, direction, customParameters} to it at call time and expects {"url":"wss://..."} back, so you can route each
call individually.
Your endpoint must be publicly reachable and wss:// (or https:// for the webhook form).
Hosts that resolve into private address space are refused twice — once when you save this config,
and again when Tone actually connects and re-resolves the DNS. That second check is the one that
matters: a hostname that's public today can point at 169.254.169.254 tomorrow.
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 '{
"numberId": "'"$NUMBER"'",
"toE164": "+919876543210",
"variables": { "order_id": "A-1042" }
}'No agentId — the number's routing mode decides what runs the call, and passing one is a 422.
variables reach your server in the start frame's customParameters, so a per-call order id or
customer name arrives with the audio.
The compliance gate runs exactly as it does for a Tone agent: your DNC list, consent, calling
window. A blocked dial is a 403 with the audit rows already written — the fact that you're
running your own AI doesn't change your obligations under TCCCPR, which is rather the point of
renting the number from us.
Inbound works with no extra setup: a call to this number opens the same WebSocket to your server.
Watching it work
Subscribe to call.completed for every finished call — answered, busy, no answer, failed — with
the disposition in the payload, so your dialer can react to the ones that didn't connect. See
Webhooks.
When something doesn't connect, the call log's endReason says why rather than dropping silently:
endReason | What it means |
|---|---|
refused_no_endpoint | The number is BYO but has no media endpoint configured |
refused_endpoint_unreachable | Tone couldn't connect, or your webhook fetch timed out |
refused_endpoint_forbidden | Your endpoint resolved to a private address, or wasn't wss:// |
refused_media_format | The carrier's sample rate and your configured rate can't be bridged |
max_duration | The call hit the 60-minute platform ceiling |
Every one of these is reported as a failed disposition with a reason. Tone never abandons a call
row silently.
Testing before you touch a phone
Point the fake caller at your server — it does the handshake, streams a WAV at real time the way a
caller would, prints every frame you send back, echoes your marks, and writes what a caller would
have heard to a WAV file:
cd voice && pnpm test:byo -- wss://bot.example.com/media --wav hello.wav --format linear16 --rate 8000With a tone_test_ key and a sandbox number, the whole path — dial, gate, bridge, billing, webhook —
runs against your endpoint with no money and no phone ringing.
Two limitations to know up front
- Campaigns don't run on BYO numbers. Launching one is refused at the button rather than
failing per-dial, which would have produced thousands of mid-run errors instead of one clear
message. Drive your own dialing loop against
POST /v1/calls. - A negotiated per-minute rate doesn't apply here. BYO bills the fixed ₹2.50 telephony+platform component. A blended full-stack discount applied to a call whose agent component we never ran would charge you more than list, so it's deliberately not used.