# Errors

> One envelope, one set of codes, and a field-level breakdown on validation failures.

Every failure comes back in the same shape, whatever went wrong. Branch on `error.code`, not on the message — messages are written for people and will change.

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request body failed validation",
    "details": [
      { "path": "title", "message": "String must contain at least 1 character" }
    ]
  }
}
```

## Status codes

| Status | Code | Means |
| --- | --- | --- |
| `400` | `BAD_REQUEST` | Malformed JSON, or a query parameter the endpoint cannot read |
| `401` | `UNAUTHORIZED` | No key, or one that is unknown, revoked or expired — deliberately indistinguishable |
| `403` | `FORBIDDEN` | The key is read only and this is a write |
| `404` | `NOT_FOUND` | No such record, or the key cannot see it |
| `409` | `CONFLICT` | The change collides with the current state |
| `422` | `VALIDATION_ERROR` | The body parsed but failed the schema; see `details` |
| `429` | `RATE_LIMITED` | Too many requests; see [Rate limits](/docs/api/rate-limits) |
| `5xx` | `INTERNAL_ERROR` | Ours. Safe to retry with backoff |

**Note**

Anything belonging to another account returns `404`, never `403`. An id can never be used to learn that a record exists — including a member id in an organization you are not in.

## Handling them

```ts Node
const response = await fetch(url, { headers });

if (!response.ok) {
  const { error } = await response.json();

  if (error.code === "RATE_LIMITED") return retryAfter(response);
  if (error.code === "VALIDATION_ERROR") throw new BadInput(error.details);

  throw new Error(`${error.code}: ${error.message}`);
}

const { data } = await response.json();
```
```python Python
response = requests.get(url, headers=headers)

if not response.ok:
    error = response.json()["error"]

    if error["code"] == "RATE_LIMITED":
        raise RetryLater(response.headers["Retry-After"])

    raise ApiError(error["code"], error["message"])

data = response.json()["data"]
```

## Retrying

**Warning**

Retry `429` and `5xx`. Never retry `4xx` — the request will fail the same way, and a retry loop on `401` will get the key rate limited on top.

Use exponential backoff with jitter, capped at a minute. Write operations accept an `Idempotency-Key` header so a retry after a timeout cannot create the same record twice.

```bash
curl -X POST https://api-notetaker.nabrah.ai/ext/v1/bots \
  -H "Authorization: Bearer $NABRAH_API_KEY" \
  -H "Idempotency-Key: 7f3c1e90-2f1a-4c0e-9a3b-9d2e5f6a7b8c" \
  -H "Content-Type: application/json" \
  -d '{"joinUrl":"https://meet.google.com/abc-defg-hij"}'
```
