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
- 401 always means "we don't know who you are" (bad/missing/revoked credential).
- 403 always means "we know who you are, but no" — and is further split into why not (missing scope vs. Plan doesn't include it vs. quota used up) so your integration can react differently (a scope problem needs a new Credential; a quota problem might just need to wait).
- 404 deliberately never distinguishes "doesn't exist" from "exists but isn't yours" — this is a tenant-isolation safeguard, not a bug. Don't build logic that assumes you can tell them apart.
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.