# Errors

> One error envelope for every failure, the codes it can carry, what to do about each, and how to use request IDs.

Source: https://dev.ody.co/api/errors

Ody uses conventional HTTP status codes: `2xx` succeeded, `4xx` means the request needs to change, `5xx` means something went wrong on Ody's side. Every error body has the same shape.

## The error envelope

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

- `error.code` *string* — Machine-readable. Branch on this, never on `message`.
- `error.message` *string* — Human-readable and safe to log. Wording may change.
- `error.requestId` *string* — Same value as the `X-Request-Id` response header.

## Codes

| Status | `code` | Meaning | What to do |
|---|---|---|---|
| `400` | `invalid_argument` | A parameter is missing or malformed, or the body isn't JSON | Fix the request; the message says which field |
| `401` | `unauthenticated` | Key missing, malformed, unknown, or revoked | Don't retry. Alert whoever owns the integration |
| `403` | `permission_denied` | Key lacks the scope; the message names it | Use a key with that [scope](https://dev.ody.co/get-started/scopes) |
| `404` | `not_found` | No such resource in this workspace (or a malformed ID) | Check the ID and that the key's workspace owns it |
| `409` | `failed_precondition` | The workspace can't do this yet: no active number, texting not registered, agent not published, no plan, identity not verified, number already taken | Surface it to a human; retrying won't help |
| `429` | `resource_exhausted` | Rate limit exceeded | Wait `Retry-After` seconds, then retry with backoff |
| `500` | `internal` | Unexpected server error | Retry with exponential backoff and jitter; cap retries |

## Request IDs

Every response — success or error — carries an `X-Request-Id` header. Log it next to your own request logs, and quote it when you contact support: it identifies the exact request on our side.

You can also send your own `X-Request-Id` (up to 64 characters: letters, digits, `_` and `-`). Ody echoes it back instead of minting one, which makes correlating retries across systems easy.

```bash
curl -i https://api.ody.co/v1/api/contacts \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "X-Request-Id: sync-2026-10-14-0001"
```

## Retrying safely

| Situation | Retry? |
|---|---|
| `429` | Yes — after `Retry-After` |
| `500`, network error, timeout | Yes — exponential backoff with jitter. For `POST /messages` and `POST /numbers`, always send an [`Idempotency-Key`](https://dev.ody.co/api/idempotency) so a retry can't double-send or double-buy |
| `400`, `401`, `403`, `404`, `409` | No — the same request will fail the same way |

## SDK errors

The SDKs raise a typed error carrying the same fields:

TypeScript:

```ts
import { OdyError } from "@ody/sdk";

try {
  await ody.contacts.get("00000000-0000-0000-0000-000000000000");
} catch (err) {
  if (err instanceof OdyError) {
    console.error(err.code, err.status, err.requestId, err.retryAfter);
  }
}
```
Python:

```python
from ody import OdyError

try:
    client.contacts.get("00000000-0000-0000-0000-000000000000")
except OdyError as err:
    print(err.code, err.status, err.request_id, err.retry_after)
```
