# Authentication

> Create, use, rotate and revoke Ody API keys. Every request sends Authorization: Bearer ody_live_… or ody_test_….

Source: https://dev.ody.co/get-started/authentication

Ody authenticates every REST and MCP request with an **API key** sent as a Bearer token. There is no OAuth flow today: keys are created by a workspace admin and belong to that workspace.

```bash
curl https://api.ody.co/v1/api/contacts \
  -H "Authorization: Bearer ody_live_…"
```

## Key format

| Prefix | Mode | Behaviour |
|---|---|---|
| `ody_live_` | Live | Everything is real: texts are sent, numbers are bought, calls are dialed |
| `ody_test_` | Test | Reads and writes real workspace data, but anything that spends money or reaches the outside world is simulated — see [Test mode](https://dev.ody.co/get-started/test-mode) |

After the prefix comes 24 random bytes (base64url). Keys are opaque: don't parse them beyond the prefix.

## Create a key

Only workspace **admins and owners** can create keys.

1. In the app, open **Settings → Developer API** (on a phone: **More → Settings → Developer API**).
2. Enter a name — "Production server", "Zapier", "Claude" — so you can tell keys apart later.
3. Choose its [scopes](https://dev.ody.co/get-started/scopes). All are pre-selected (full access); deselect to narrow the key.
4. Choose **Live** or **Test**.
5. Click **Create key** and copy the secret from the banner: **"Copy your new key now — you won't see it again."**

Ody stores only a SHA-256 hash of the key. The plaintext is shown once and can never be retrieved — if you lose it, revoke it and create a new one. The key list shows a masked hint (`ody_live_…WXYZ`), a **Test** badge for test keys, the scopes, and when the key was last used.

## What a key can do

A key **acts as the member who created it**. Every call is checked against that member's role and the workspace's row-level security, exactly as if they clicked the same button in the app. A key can never reach another workspace.

## Errors

| Status | Code | Meaning |
|---|---|---|
| `401` | `unauthenticated` | Missing, malformed, unknown or revoked key. Don't retry — fix the configuration |
| `403` | `permission_denied` | The key is valid but lacks the scope this endpoint needs. The message names the scope |

```json
{ "error": { "code": "permission_denied", "message": "This API key is missing the 'messages:send' scope", "requestId": "req_8c1f…" } }
```

## Rotate and revoke

Revoking is immediate: the next request with that key gets `401`. Because a key's plaintext is shown only once, rotation is:

1. Create a new key with the same scopes.
2. Deploy it to your service.
3. Revoke the old key in **Settings → Developer API**.

## Keep keys safe

> **Server-side only:**
Anyone holding a key can act as its creator within its scopes. Never put a key in browser JavaScript, a mobile app, a public repository or a shared document.

- Store keys in a secrets manager or environment variable.
- Use **one key per integration** so you can revoke one without breaking the others, and so "last used" stays meaningful.
- Give each key the **fewest scopes** it needs — a reporting dashboard needs only `conversations:read`.
- Use **test keys** in development and CI.

## CORS

`/v1/api` and `/v1/mcp` accept requests from any origin, because they authenticate with a bearer key rather than cookies. That makes browser-based tools possible — it does **not** make it safe to ship a key to end users' browsers.
