Authentication
API keys, the live/test split, scopes, and rotation without downtime.
Every request carries an API key as a bearer token:
curl "$TONE_API/v1/agents" \
-H "Authorization: Bearer tone_test_a1b2c3..."There is no other authentication mode for the API. Keys never go in a URL or a query parameter — URLs end up in access logs, browser history, and referrer headers.
The environment is the prefix
tone_test_... the sandbox
tone_live_... real carriers, real moneyThis is structural, not a flag you pass. A tone_test_ key routes every operation to the sandbox:
no carrier is contacted, no phone rings, no wallet is debited, and prices are simulated. A
tone_live_ key does the real thing. Nothing in a request body can bridge the two — a test key
naming a live number fails with environment_mismatch, and so does the reverse.
Everything a key READS is pinned to its own environment: calls, numbers, campaigns, events,
compliance checks, sealed evidence, consents and suppression entries. A test key sees only what
the test environment produced. A resource of the other environment named in a URL path answers
404.
Some things are shared by both environments, and it is worth knowing which:
- Agents and knowledge bases are one object in both. Publishing an agent from a test key publishes it for live calls too.
- Webhook endpoints receive both test and live events; the envelope's
environmentfield says which. Because an endpoint receives live events, adding, changing or removing one, and reading or rolling its signing secret, needs a live key or the dashboard — a test key gets403. - The suppression (DNC) list applies to both. An opt-out or a suppression recorded with a test key still protects that person from live calls; a test key just cannot read the entries the live environment added.
- Consents are per environment. A consent recorded with a test key counts for test calls only, so a sandbox credential can never authorise a real promotional call past the DND register.
The consequence worth planning around: going to production is a key swap and nothing else. Test mode exercises the whole lifecycle — call states, dispositions, webhooks, CDR rows, priced (but never charged) debits — so an integration that works in test works live.
Test keys are available from day one, before your KYC and DLT registration are complete. Live keys can be created at any time too; what verification gates is buying numbers, launching live campaigns, and SIP trunks. Build and ship your integration while the paperwork is in flight — see Test mode.
Getting a key
Create keys in the dashboard under Developer → API keys, or programmatically:
curl "$TONE_API/v1/api-keys" \
-X POST -H 'content-type: application/json' \
-d '{"name":"orders-service","environment":"test","scopes":["write"]}'POST /v1/api-keys authenticates with a dashboard session, not a key. Keys cannot mint keys —
a leaked key can do damage, but it cannot manufacture more credentials or escalate its own scope.
A key presented to this endpoint gets a 401.
The response is the only time the full key exists:
{
"data": {
"id": "7c3d...",
"name": "orders-service",
"environment": "test",
"scopes": ["read", "write"],
"start": "tone_test_a1b2",
"enabled": true,
"lastUsedAt": null,
"expiresAt": null,
"createdAt": "2026-08-24T09:41:07.812Z",
"key": "tone_test_a1b2c3d4..."
}
}Store key in your secret manager immediately. We keep only a hash of it and the start
fragment; GET /v1/api-keys will show you the fragment forever and the key never again. If you
lose it, roll it.
Pass expiresAt (ISO 8601) at creation if the key should stop working on a date — useful for a
contractor's key or a time-boxed migration.
Scopes
Three scopes, and each implies the ones below it:
| Scope | Grants | Typical holder |
|---|---|---|
read | Every GET. Call logs, agents, numbers, wallet balance, events, compliance history. | Analytics, dashboards, monitoring |
write | Everything read does, plus creating and updating agents, knowledge bases, campaigns, and placing calls | Your application |
admin | Everything write does, plus spending money and changing where data flows: buying and releasing numbers, provisioning SIP trunks, managing webhook endpoints, reading signing secrets | Deployment automation, ops |
Omitting scopes at creation gives you read — least privilege by default.
A scope you don't have is a 403 with error.code: "insufficient_scope", never a silent no-op.
Two rules constrain what a key can be:
- A key can never out-rank the person who minted it. Requested scopes are clamped to the
minting user's own role ceiling — Owners and Admins can mint
adminkeys, Developers can mint up towrite, everyone else up toread. This is re-checked on every request, not just at creation, so a key cannot outlive a demotion. PATCH /v1/api-keys/:idmay only narrow scopes. Widening is a new-key event: mint a replacement and roll. A quietly edited old credential can never gain authority.
Why buying a number needs admin
Purchasing provisions a real line with a real monthly rental, and releasing one takes down a
published business line that customers may be calling. Both are admin. Your application, holding
a write key, can create agents, place calls, and run campaigns all day — but cannot spend from
the wallet or delete the number those calls come from.
Rotating a key
Rolling mints a replacement and keeps the old key working for an overlap window, so rotation never requires a coordinated deploy:
curl "$TONE_API/v1/api-keys/7c3d.../roll" \
-X POST -H 'content-type: application/json' \
-d '{"expireOldIn":"24h"}'expireOldIn accepts now, 1h, 24h (default), or 7d. Both keys verify during the window;
deploy the new one at your own pace.
Use now when a key has leaked — that's the whole point of the setting. To kill a key outright:
curl -X DELETE "$TONE_API/v1/api-keys/7c3d..."Revocation takes effect immediately: the response is a 204, and the next request with that key is
a 401.
Keeping keys out of trouble
- Rotate on a schedule — a
7doverlap makes quarterly rotation a non-event. - One key per system, named after the system. When something needs revoking at 2am, you want to revoke the thing that leaked, not everything.
- Never in the browser. A key in front-end JavaScript is a public key. Calls, campaigns, and number purchases are all authorized by it. Put your server between your users and this API.
- Never in a git repository. If a key does land in one, roll it with
expireOldIn: "now"— scrubbing history is slower than the key is valuable.
If a key leaks publicly
Tone participates in GitHub's secret-scanning partner program. If one of your keys is pushed to a public repository, GitHub tells us within seconds and:
- A live key is revoked immediately, without waiting to ask. It can place calls, buy numbers and spend your wallet, so we don't leave it alive for the length of an email. Anything using it stops working — mint a replacement and deploy it. We email your owners and admins to say what happened.
- A test key is revoked too. It cannot spend money or ring a phone, but it can change the agents your live calls use and read your knowledge bases, which both environments share. Mint a replacement for whatever used it. We email your owners and admins either way.
Deleting the file afterwards is not enough on its own — the key remains in your git history, which is why the credential itself has to be replaced rather than hidden.