# Errors

> One envelope for every failure, with a stable code to branch on.

Source: https://www.hey.support/docs/api/errors

---

Every failing request — on this API and on every other HTTP endpoint we serve — returns the same JSON envelope.

```json
{
  "error": "Invalid API key.",
  "code": "unauthorized",
  "docs_url": "https://www.hey.support/docs/api"
}
```

## Branch on `code`, never on `error`

`code` is a stable symbol. It is part of the contract and will not change under `v1`.

`error` is prose written for a human reading a log. **It may be reworded at any time**, so matching on its text will break without warning.

`docs_url` points at the page explaining that failure.

<Callout>
  This is deliberately not RFC 9457 `problem+json`. Changing the content type and body shape would break every existing consumer — the Zapier app, the WordPress plugin and the mobile client all read `.error` — and buys nothing over a stable `code`.
</Callout>

## Codes

| HTTP | `code`               | What it means                                                          |
| ---- | -------------------- | ---------------------------------------------------------------------- |
| 400  | `bad_request`        | The body or query failed validation. The message names the field.      |
| 401  | `unauthorized`       | Missing, malformed, or revoked API key.                                |
| 403  | `forbidden`          | The key is valid but the workspace's plan does not include API access. |
| 404  | `not_found`          | No such resource — **or** it belongs to another workspace.             |
| 405  | `method_not_allowed` | Wrong verb for that path.                                              |
| 406  | `not_acceptable`     | We cannot produce the representation you asked for.                    |
| 429  | `rate_limited`       | Over 1,000 requests in the hour. Check `X-RateLimit-Reset`.            |
| 5xx  | `internal_error`     | Our fault. Safe to retry with backoff.                                 |

## 404 versus 403

A resource in another workspace returns **`404`**, not `403`. That is intentional: a `403` would confirm the id exists, which turns the API into a way to enumerate other people's data.

So a `404` means one of two things — the id is wrong, or it is not yours.

## Retrying

`429` and `5xx` are worth retrying with exponential backoff. `4xx` other than `429` will fail identically however many times you send it — fix the request instead.
