Errors

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

HTTPcodeWhat it means
400bad_requestThe body or query failed validation. The message names the field.
401unauthorizedMissing, malformed, or revoked API key.
403forbiddenThe key is valid but the workspace's plan does not include API access.
404not_foundNo such resource — or it belongs to another workspace.
405method_not_allowedWrong verb for that path.
406not_acceptableWe cannot produce the representation you asked for.
429rate_limitedOver 1,000 requests in the hour. Check X-RateLimit-Reset.
5xxinternal_errorOur 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.