# Errors

# Errors

One shape, whatever went wrong:

```json
{
  "error": {
    "code": "invalid_status",
    "message": "Unknown status \"bidon\"."
  }
}
```

`code` is stable and meant to be branched on. `message` is a sentence for a
human and may be reworded — never match on it.

## The codes

| Status | Code | What it means |
|---|---|---|
| 400 | `invalid_body` | The body is not a JSON object. |
| 400 | `invalid_status`, `invalid_priority` | A value outside the allowed set. |
| 400 | `invalid_cursor` | The cursor is not one we handed out. |
| 400 | `invalid_assignee`, `invalid_organization`, `invalid_category` | An id that belongs to no such object in this workspace. |
| 400 | `empty_patch` | A `PATCH` with nothing to change. |
| 401 | `unauthorized` | Missing, malformed, unknown or revoked key. |
| 403 | `forbidden` | The key lacks the scope this route needs. |
| 403 | `workspace_suspended` | The workspace is suspended; nothing is served. |
| 403 | `requester_blocked` | The contact is blocked from opening tickets. |
| 404 | `not_found` | No such object **in this workspace**. |
| 409 | `email_taken`, `slug_taken` | A unique value already belongs to another row. |
| 409 | `contact_has_tickets` | Deleting would erase a support history; ask for it explicitly. |
| 429 | `rate_limited` | Over 600 requests a minute. `Retry-After` says how long. |
| 500 | `internal_error` | Ours. Retry, and tell us if it persists. |

## What to retry

- **429** — yes, after `Retry-After`.
- **500** — yes, with backoff. Twice, then raise it rather than hammer.
- **4xx anything else** — no. The request will fail identically until you change
  it; retrying only fills your logs and our rate limit.

## A note on 404

A 404 means "not in *this* workspace", not "does not exist anywhere". An id from
another workspace is indistinguishable from one that was never created, and
deliberately so: the API must not confirm that someone else's ticket exists.
