# Conventions

Source: https://docs.usetone.ai/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.ai
```

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

```bash
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](/authentication).

## The response envelope

Every successful response is wrapped:

```json
{ "data": { "id": "8f14...", "status": "queued" } }
```

List endpoints add `meta.pagination`:

```json
{
  "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](/errors).

## Casing: bodies are camelCase, query params are snake_case

This split is intentional and consistent:

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

```bash
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: 480` is
  ₹4.80. There are no floats in this API's money path. `currency` is `INR`.
- **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 a `400`.

## 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: 1756029667
```

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

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

```json
{
  "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`](/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](/tone.postman_collection.json) | The same thing, pre-converted, with the bearer token as a collection variable. |
| [`llms.txt`](/llms.txt) | An index of every page, [llmstxt.org](https://llmstxt.org) convention. |
| [`llms-full.txt`](/llms-full.txt) | Every guide, in full, in one file. |
| Any page + `.md` | The Markdown of that page — [`/errors.md`](/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](/versioning).
