Errors
Every error response uses the same envelope, with a stable machine-readable code. Use the code, not the message, for branching logic.
Envelope
{
"success": false,
"errorCode": "RATE_LIMITED",
"message": "Rate limit exceeded for read endpoints. Retry in 42 seconds.",
"details": {
"category": "read",
"retryAfter": 42
},
"requestId": "req_2N7s8x9KqR"
}The requestId is the most useful field when reporting an issue — include it in any support email and we can pull the full request and downstream traces in seconds.
Status code conventions
| Status | Meaning | Retry? |
|---|---|---|
400 | Validation failure — body or query parameters are malformed | No, fix the request |
401 | Missing, malformed, or expired credentials | No, refresh the credential |
403 | Authenticated but not authorized — wrong tier, scope, or corporate | No, escalate |
404 | Resource doesn't exist or your credential can't see it | No |
409 | State conflict — booking a full session, double-cancelling, etc. | No, refetch state |
422 | Business rule violation — request was well-formed but not allowed | No |
429 | Rate limit exceeded | Yes, after Retry-After |
5xx | Server-side failure | Yes, exponential backoff with jitter |
Common error codes
| Code | Status | Notes |
|---|---|---|
INVALID_REQUEST | 400 | details.fields[] lists which fields failed validation |
UNAUTHENTICATED | 401 | Missing or malformed credential |
TOKEN_EXPIRED | 401 | Refresh and retry |
PREMIUM_REQUIRED | 403 | Member API on a Basic tier key — upgrade required |
FORBIDDEN | 403 | Credential authenticated but lacks scope |
NOT_FOUND | 404 | Resource doesn't exist or isn't visible to this credential |
SESSION_FULL | 409 | Booking failed — session is at capacity. Try the waitlist endpoint |
ALREADY_BOOKED | 409 | Member already has an active booking for this session |
RATE_LIMITED | 429 | See Rate limits |
UPSTREAM_TIMEOUT | 504 | Downstream service didn't respond in time — retry safe |
Retry strategy
Only retry the classes marked retryable above. For 5xx and UPSTREAM_TIMEOUT, use exponential backoff with full jitter, capped at 30 seconds, with a maximum of 5 attempts. For 429, honour Retry-After exactly on the first retry; if it 429s again, fall back to exponential backoff.
Use the Idempotency-Key header on POST and PATCH retries so duplicate calls don't create duplicate resources. Any unique string up to 255 characters is fine — UUIDs are conventional. Keys are remembered for 24 hours.
Validation errors
INVALID_REQUEST always includes a details.fields array so you can surface field-level messages in your UI:
{
"success": false,
"errorCode": "INVALID_REQUEST",
"message": "Validation failed.",
"details": {
"fields": [
{ "path": "email", "message": "must be a valid email" },
{ "path": "phone", "message": "must include country code" }
]
},
"requestId": "req_2N7s8x9KqR"
}