Appearance
Errors
Shape
Errors come back as JSON with three fields:
json
{
"statusCode": 400,
"message": "size must be one of small | medium | large; got 'extra-large'",
"error": "invalid_size"
}statusCode— the HTTP status code, repeated in the body.message— human-readable. Safe to surface in CI logs.error— short machine-readable code for branching. Stable across versions; themessagemay be edited.
For validation errors (400), message is sometimes an array of per-field issues:
json
{
"statusCode": 400,
"message": [
"slug must be 2–40 chars, lowercase alphanumerics + dashes",
"adminEmail must be an email"
],
"error": "Bad Request"
}Status code guide
| Status | What it means | What to do |
|---|---|---|
400 Bad Request | Validation failed — wrong types, missing fields, bad enums | Fix the request. The message array tells you which fields. |
401 Unauthorized | Missing, invalid, or expired token | Mint a new token; check Authorization header is set. |
403 Forbidden | Authenticated but not allowed — wrong role, missing scope, federation/spoke not in your visibility | Check the token's scope and the user's role. |
404 Not Found | The resource doesn't exist or you can't see it | We return 404 instead of 403 when the existence of the resource is sensitive. |
409 Conflict | The request would violate a uniqueness or lifecycle invariant | E.g. signing up with an email that's already in use; trying to provision a spoke at a slug already taken. |
422 Unprocessable Entity | Reserved — currently unused | If you see this, it's a bug. Report it. |
429 Too Many Requests | Hit a rate limit | Back off. See Rate limits. |
500 Internal Server Error | The control plane itself failed | Check /api/healthz. Open a ticket if it persists. |
502/503/504 | Ingress / load shedding | Retry with exponential backoff. |
Common error codes
A non-exhaustive list of error codes you might see:
| Code | Status | Meaning |
|---|---|---|
invalid_size | 400 | The size field wasn't one of small/medium/large |
federation_id_mismatch | 400 | Path :federationId doesn't match the body's federationId |
email_taken | 409 | Signup attempted with an in-use email |
hub_not_found | 404 | The federation has no hub provisioned yet |
unauthorized | 401 | Generic auth failure — wrong/missing token |
forbidden_scope | 403 | Token lacks the scope required for this endpoint |
Codes are added as endpoints are added. We don't promise the full list is documented; treat unknown codes the same way you treat the matching status.
Idempotency and retries
Most POST endpoints are not idempotent by default — provisioning a spoke twice creates two spokes if the slugs differ. The exceptions are documented per-endpoint in the reference. If you need at-most-once semantics, take the lock on your side or check existence with a GET before posting.
Network-level retries (e.g. retrying a 5xx) are safe for GET and DELETE. For POST/PATCH, only retry when you're confident the request didn't reach the server (connection refused, DNS fail).