# Errors

Every error has the same shape:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This credential is missing the \"contacts:write\" scope.",
    "request_id": "req_8f2c…",
    "details": { "required": "contacts:write", "granted": ["contacts:read"] }
  }
}
```

Switch on `code`. It is a closed set and part of the contract; `message` is for humans and may be
reworded.

| code | status | what it means |
|---|---|---|
| `invalid_request` | 400 | The body or query string failed validation. `details.issues` has the specifics. |
| `unauthenticated` | 401 | Missing, expired, revoked, or malformed credential. |
| `insufficient_scope` | 403 | Valid credential, wrong scopes. `details.required` names the one you need. |
| `access_denied` | 403 | The workspace revoked your app, or never authorized it. |
| `not_found` | 404 | No such object **for this workspace**. |
| `conflict` | 409 | A uniqueness constraint — e.g. two API keys with the same name. |
| `unprocessable` | 422 | Well-formed but the domain refused it. |
| `compliance_blocked` | 422 | A send was refused by the compliance gate. `message` names the reason. |
| `rate_limited` | 429 | Slow down; see `Retry-After`. |
| `server_error` | 500 | Ours. `request_id` is what support needs. |

## 404 vs 403

Another workspace's object id returns **404, not 403**. A 403 would confirm the row exists
somewhere, which turns every id endpoint into a platform-wide existence oracle. "Not found" is the
honest answer from your credential's point of view.

## request_id

Every response — success or failure — carries `X-Request-Id`, and errors repeat it in the body.
Log it. It is the fastest path to an answer from support.
