Errors

Every non-2xx response from /api/v1/* uses the same stable envelope:

{
  "error": {
    "code": "SCOPE_DENIED",
    "message": "A human-readable description."
  }
}

Branch on error.code, not on the message text — the message is for humans, the code is for your code.

Codes

Code HTTP status Meaning
MALFORMED_REQUEST 400 The request itself is invalid (bad cursor, bad limit, unknown category key, etc.) — checked before authentication/entitlement.
UNAUTHENTICATED 401 The credential is missing, invalid, revoked, or expired.
NOT_FOUND 404 The resource doesn't exist. Also returned for a resource that exists but belongs to a different Customer — these two cases are deliberately indistinguishable to you, by design.
CONFLICT 409 The request is well-formed, but the current state doesn't allow it (e.g. the Site already has an active subscription).
CAPABILITY_NOT_ENTITLED 403 Authenticated and scoped, but your Site's Plan doesn't include this capability at all.
QUOTA_EXHAUSTED 403 The capability is included, but this period's quota is used up.
SCOPE_DENIED 403 The credential is valid, but doesn't hold the scope this endpoint requires.
RATE_LIMITED 429 The security rate limit was exceeded. A Retry-After header may be present. See Rate Limits.
INTERNAL_ERROR 500 (occasionally 503, e.g. when a payment provider is temporarily unavailable) Something went wrong on our side. Safe to retry after a short delay.

UNAUTHORIZED exists in the underlying error-code vocabulary but is not currently returned by any live endpoint — treat it as reserved, not something you'll encounter today.

Why 401/403/404 are split the way they are

What's safe to log

error.message is safe to log — it never contains a stack trace, SQL, a connection string, a credential, an internal provider error, or a filesystem path, by construction.