Conventions
Base URL, the response envelope, pagination, casing, and rate limits — the things that are true of every endpoint.
Everything on this page holds for every endpoint, so the reference pages don't repeat it.
Base URL
https://apibeta.usetone.aiEvery path is under /v1. The examples in these docs use a shell variable so you can point them
at whichever environment you're working against:
export TONE_API=https://apibeta.usetone.ai
export TONE_KEY=tone_test_.../v1 is a promise, not a version number
There will be no /v2. Changes to this API are additive: new endpoints, new optional request
fields, new response fields, new enum members. We will not remove a field you receive, rename one,
change a type, or make an optional request field required.
The practical consequence for your code: treat every enum as open. A disposition you have
never seen, a new status, a new webhook type — these can appear without warning and are not
breaking changes. Handle the unknown case (log it, ignore it, fall through to a default) rather
than throwing. A switch with no default is the most common way an integration breaks against
an additive API.
The contract is enforced mechanically: backend/openapi.json is committed and diffed on every
change, so a field cannot disappear by accident.
Authentication
Authorization: Bearer tone_live_... or Authorization: Bearer tone_test_... on every request.
See Authentication.
The response envelope
Every successful response is wrapped:
{ "data": { "id": "8f14...", "status": "queued" } }List endpoints add meta.pagination:
{
"data": [ { "id": "8f14..." }, { "id": "1c92..." } ],
"meta": { "pagination": { "cursor": "eyJpZCI6...", "hasMore": true, "limit": 25 } }
}Two exceptions, both deliberate: CSV exports (/v1/calls/export.csv and friends) return the CSV
itself, and binary downloads (a call recording) return the bytes. Wrapping either would corrupt
the download.
Errors replace data entirely — see the error catalog.
Casing: bodies are camelCase, query params are snake_case
This split is intentional and consistent:
# query params: snake_case
curl "$TONE_API/v1/calls?agent_id=$AGENT&include_unstarted=true&limit=50" ...
# request bodies: camelCase
curl "$TONE_API/v1/calls" -d '{"agentId":"...","numberId":"...","toE164":"+919876543210"}'Response bodies are camelCase, matching request bodies. The snake_case exceptions in responses are
the developer-contract fields that are snake_case everywhere in this API: error.type,
error.code, error.doc_url, error.request_id, and the webhook envelope's created_at.
Query strings are strict. A misspelled parameter is a 400, not a silent no-op. This is on
purpose: agentId=... instead of agent_id=... would otherwise quietly return your entire
unfiltered call log, which is the worst possible outcome for a filter.
Pagination
Cursor-based, newest first. Pass limit (1–100, default 25), then feed
meta.pagination.cursor back as cursor until hasMore is false.
curl -H "Authorization: Bearer $TONE_KEY" \
"$TONE_API/v1/calls?limit=100"
# → meta.pagination.cursor = "eyJpZCI6..."
curl -H "Authorization: Bearer $TONE_KEY" \
"$TONE_API/v1/calls?limit=100&cursor=eyJpZCI6..."The cursor is opaque — don't parse it, don't construct one, don't store it as a bookmark past the
end of a paging session. Cursors are used instead of offsets so that pages stay stable while new
rows arrive: with ?page=2, a call that lands mid-scan shifts every subsequent row and you silently
skip one.
Some list endpoints also return meta.pagination.total (the suppression list does, where "how many
numbers am I blocking?" is a real question). Most don't: on an append-only log, a COUNT(*) is a
second query whose answer is stale before you read it.
Timestamps, money, and phone numbers
- Timestamps are RFC 3339 / ISO 8601 strings in UTC:
"2026-08-24T09:41:07.812Z". - Money is always paise, always an integer, always named
...Paise—billedPaise: 480is ₹4.80. There are no floats in this API's money path.currencyisINR. - Phone numbers are E.164 with the country code and a leading
+:+919876543210. This is both what you send and what you receive. A number without the+, or with spaces, is a400.
Rate limits
Two budgets, and they fail in different ways for different reasons.
Requests per minute — a per-organization token bucket, separate per environment. Every keyed response carries both header families:
RateLimit-Policy: "org";q=600;w=60
RateLimit: "org";r=594;t=41
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 594
X-RateLimit-Reset: 1756029667The first pair is the IETF structured-field form (q = quota, w = window seconds, r =
remaining, t = seconds until reset); the X- pair is the older GitHub-style family, where
X-RateLimit-Reset is an absolute epoch second rather than a duration. Use whichever your HTTP
client already understands — they describe the same bucket.
Exhausting it returns 429 with error.code: "rate_limited", a Retry-After header, and
error.details.retry_after_seconds. Back off and retry — a 429 here means slow down.
Concurrent calls — how many calls can be in flight at once, counted in two separate pools
(Tone-agent calls and BYO calls). Exceeding it returns error.code: "concurrent_call_limit_reached",
which is deliberately a different code from rate_limited because the fix is different: you are
not sending requests too fast, you have too many calls up. Wait for calls to end, or raise the quota.
Read both budgets, live, at any time:
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/limits"{
"data": {
"environment": "test",
"requests": { "limitPerMinute": 300, "remaining": 297 },
"concurrency": {
"agentCalls": { "limit": 10, "inUse": 2 },
"byoCalls": { "limit": 10, "inUse": 0 }
}
}
}Don't hardcode these numbers — read them. They differ between test and live, and they change
when your plan does.
Request IDs
Every response — success or failure — carries a request_id, and every error envelope repeats it
in the body. Log it. Quoting one in a support conversation turns "a call failed yesterday" into a
single row in our logs.
Machine-readable
Everything on this site is available in a form a tool can read.
openapi.json | The OpenAPI 3.1 spec — 92 operations, every field described, with example bodies. Import it directly into Postman, Insomnia or an SDK generator. |
| Postman collection | The same thing, pre-converted, with the bearer token as a collection variable. |
llms.txt | An index of every page, llmstxt.org convention. |
llms-full.txt | Every guide, in full, in one file. |
Any page + .md | The Markdown of that page — /errors.md. |
/mcp | An MCP server, so a coding agent can read these docs in your editor. Two tools, no credential needed. |
The spec is generated from the running code and diffed on every change, so it cannot describe an API we do not have. See Versioning.