One envelope for every failure, with a stable code to branch on.
Every failing request — on this API and on every other HTTP endpoint we serve — returns the same JSON envelope.
{
"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.
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.
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.