Envelope
{
"error": {
"code": "validation_error",
"message": "Validation failed.",
"details": { "email": ["Already in use."] },
"request_id": "9f2c5b4d-77b2-4f48-a4d3-8b3f01b8d8f2"
}
}
code— stable string. Branch on this. Never branch on the human-readable message; we may rephrase.message— short, end-user safe summary.details— optional. Forvalidation_error, a dict of field → list of errors. Otherwisenull.request_id— paste into support tickets so we can grep server logs in seconds.
Codes you'll see
| Code | HTTP | Meaning |
|---|---|---|
| validation_error | 422 | Field-level validation failed. details has the per-field reasons. |
| invalid_credentials | 400 | Login email + password didn't match a user. |
| authentication_failed | 401 | Bearer token missing, expired, or revoked. Refresh or log in again. |
| permission_denied | 403 | Authenticated, but not allowed. |
| subscription_required | 402 | Action needs an active subscription. Direct the user to /pricing/. |
| theme_premium_required | 403 | Theme is part of a premium plan; current plan doesn't unlock it. |
| not_found | 404 | Resource missing — or you don't own it (we 404 instead of 403 to avoid id-enumeration). |
| conflict | 409 | Action would violate an invariant (e.g. two live subscriptions at once). |
| rate_limited | 429 | Throttle hit. Honor Retry-After — see Rate limits. |
| service_unavailable | 503 | Upstream integration (Twilio/Firebase/etc.) is down. Retry with backoff. |
| app_error | 400 | Generic business-rule violation. Read message for the specifics. |
New codes will appear over time. If you see one we haven't documented, treat it as the closest super-category (e.g. unknown *_required → handle like subscription_required) and tell us.
Client patterns
JavaScript
async function api(url, options) {
const res = await fetch(url, options);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
const code = body?.error?.code ?? `http_${res.status}`;
if (code === "authentication_failed") return refreshAndRetry(url, options);
if (code === "subscription_required") return goToPricing();
if (code === "rate_limited") return backoff(res.headers.get("Retry-After"));
throw new ApiError(code, body?.error?.message);
}
return res.json();
}
Python
import requests
class ApiError(Exception):
def __init__(self, code: str, message: str, request_id: str | None):
super().__init__(f"[{code}] {message}")
self.code = code
self.request_id = request_id
def call(method, url, **kw):
r = requests.request(method, url, **kw)
if r.ok:
return r.json()
body = (r.json() or {}).get("error", {})
raise ApiError(
code=body.get("code") or f"http_{r.status_code}",
message=body.get("message", r.reason),
request_id=body.get("request_id"),
)
Bug? Drop us a note via the contact form with the offending request_id.