# Errors

Every error response — including auth failures, rate-limit responses, and
validation errors — has this shape:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This action requires the 'bookings:create' scope",
    "details": { "required_scope": "bookings:create" },
    "request_id": "req_…"
  }
}
```

- **`code`** — stable machine identifier. Switch on this, not on `message`.
- **`message`** — human-readable explanation. Wording may change.
- **`details`** — endpoint-specific extra fields (e.g. the offending parameter
  name, or the required scope). May be empty `{}`.
- **`request_id`** — unique per request. **Quote this** when reporting an
  issue.

The HTTP status is set per RFC 7231 — switch on it for routing (`5xx` ⇒ retry,
`4xx` ⇒ fix the request) and use `code` for granular handling.

## Common error codes

### Authentication (`401`)

| Code | Meaning |
|---|---|
| `invalid_token` | Missing, malformed, or unknown bearer token. |
| `token_expired` | Token is past its expiry. |
| `token_revoked` | PAT or OAuth token has been revoked. |

These responses also include `WWW-Authenticate: Bearer realm="42min",
error="…"` — useful for clients that follow the RFC 6750 challenge protocol.

### Authorization (`403`)

| Code | Meaning |
|---|---|
| `insufficient_scope` | Token is valid but missing the required scope. `details.required_scope` names which one. |
| `forbidden` | Generic refusal — auth and scope are fine, but the actor isn't allowed to do this. |

### Validation (`400` / `422`)

| Code | Meaning |
|---|---|
| `validation_error` | Request body failed validation. |
| `invalid_request` | The request itself is malformed (bad headers, bad shape). |
| `invalid_query_param` | A query parameter is missing or malformed. `details.param` names which one. |
| `attendee_email_invalid` | `attendee.email` failed format check. |
| `invalid_if_match` | `If-Match` is not a valid integer. |
| `field_immutable` | A PATCH tried to write a field that can only change via dedicated endpoints (e.g. `start`). `details.fields` lists them. |
| `event_type_disallows_reschedule` | The event type has `disableRescheduling`. |

### Conflict / state (`409`)

| Code | Meaning |
|---|---|
| `slot_unavailable` | Requested slot is no longer free. |
| `event_type_inactive` | Event type's status is not `on`. |
| `booking_in_past` | Booking starts (or started) in the past. |
| `booking_already_cancelled` | Reschedule attempted on a non-confirmed booking. |
| `version_conflict` | `If-Match` doesn't match the current booking version. |
| `idempotency_in_progress` | Same `Idempotency-Key` still being processed. `Retry-After: 1`. |
| `idempotency_key_conflict` | Same key, different body within the 24h window. |
| `pat_name_taken` | Another PAT in this account has the same name. |
| `pat_limit_exceeded` | Account has 42 active PATs already. |

### Not found (`404`)

| Code | Meaning |
|---|---|
| `not_found` | Generic. |
| `event_type_not_found` | Event type doesn't exist or is in another account. |
| `booking_not_found` | Booking doesn't exist, was hard-deleted, or its UID is malformed. |
| `interaction_not_found` | OAuth consent interaction expired or doesn't exist. |

We deliberately collapse "wrong tenant" into `not_found` — a 404 doesn't leak
the existence of resources in other accounts.

### Precondition / payload (`412` / `413` / `415` / `428`)

| Code | Meaning |
|---|---|
| `missing_if_match` | PATCH without `If-Match`. (`428 Precondition Required`.) |
| `missing_idempotency_key` | Write without `Idempotency-Key`. (`400`.) |
| `invalid_idempotency_key` | `Idempotency-Key` longer than 255 chars. (`400`.) |

### Rate-limiting (`429`)

| Code | Meaning |
|---|---|
| `rate_limited` | Bucket exhausted. `Retry-After` header set; see [Rate limits](/help/api/rate-limits). |

### OAuth (`400` / `401`)

| Code | Meaning |
|---|---|
| `invalid_client` | Bad `client_id` / `client_secret`, or unknown client. |
| `invalid_grant` | Bad auth code, bad refresh token, PKCE failure, redirect-URI mismatch, or replay detected. |
| `invalid_scope` | Requested scope not allowed for this client, or unknown scope name. |
| `invalid_redirect_uri` | redirect URI is not registered or fails scheme rules. |
| `unsupported_grant_type` | `grant_type` is not `authorization_code` or `refresh_token`. |
| `unsupported_response_type` | `response_type` is not `code`. |
| `invalid_client_metadata` | DCR request rejected (scope not in allowlist, etc.). |
| `access_denied` | User declined on the consent screen. Returned via redirect, not a JSON body. |

### Server (`5xx`)

| Code | Meaning |
|---|---|
| `internal_error` | Something went wrong server-side. Quote `request_id` to support. |
| `slot_lock_timeout` | Could not acquire the per-slot lock when creating/rescheduling a booking. `Retry-After: 1`. (`503`.) |

## Recommended client handling

1. **`5xx` or network failure** — retry with the same `Idempotency-Key` after
   a brief delay (exponential backoff, capped). Writes are safe to retry as
   long as the key stays the same.
2. **`429`** — wait for `Retry-After` or `X-RateLimit-Reset`, then retry.
3. **`401 token_expired`** — refresh (OAuth) or rotate (PAT). For OAuth, if
   the refresh attempt itself returns `invalid_grant` with "session has been
   revoked", start the authorize flow from scratch.
4. **`409 version_conflict`** — re-`GET` and reapply your patch on the new
   version.
5. **`409 slot_unavailable`** — the slot was taken between your check and
   your create. Pick another slot (the booking page or `/v1/slots` will reflect
   the new state).
6. **`4xx` other** — surface to the user; usually a bug in the request.

Always log `request_id` from `error.request_id` (or
`meta.request_id` on success) — it's the fastest way for support to trace what
happened.
