# Authentication

Source: https://docs.usetone.ai/authentication

> API keys, the live/test split, scopes, and rotation without downtime.

Every request carries an API key as a bearer token:

```bash
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 money
```

This 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.

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
unlock at verification. Build and ship your integration while the paperwork is in flight — see
[Test mode](/test-mode).

## Getting a key

Create keys in the dashboard under **Developer → API keys**, or programmatically:

```bash
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:

```json
{
  "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](#rotating-a-key).

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 `admin` keys, Developers can mint
  up to `write`, everyone else up to `read`. This is re-checked on *every* request, not just at
  creation, so a key cannot outlive a demotion.
- **`PATCH /v1/api-keys/:id` may 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:

```bash
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:

```bash
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 `7d` overlap 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 not revoked.** It reaches only the sandbox: it cannot spend money, ring a phone,
  or read live data. Breaking your CI over it would do more harm than the leak. We email you so you
  can rotate it when convenient.

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.
