Error codes
All error responses share a consistent shape:
{ "error": "<code>", "details": [ ... ] }
with Cache-Control: no-store so transient errors never poison the edge cache.
4xx — your request
400 invalid_query
Zod validation failed on a query parameter. details[] includes per-issue {path, message}. Common causes:
- Unknown query parameter (schemas are
.strict()— unknown keys are rejected; if you sent?foo=1and we didn't documentfoo, that's a 400) - Value out of range (
limit > 100,year_from > year_to, etc.) - Unicode homoglyph attempt (e.g. fullwidth
style— see authentication §normalization for why we NFKC-normalize)
Fix: read the details[] array; the path tells you which parameter failed; the message tells you why.
400 invalid_facet
A facet name was provided but doesn't match the known allowlist (genre, style, format, country, year_from, year_to, label). details includes raw (your input) and normalized (the NFKC-normalized form we tried to match).
400 invalid_master_id
The path param for /api/v1/masters/{id} failed integer coercion. Must be positive integer ≤ 2,000,000,000.
400 invalid_batch_size
/api/v1/masters/batch body failed validation. Either malformed JSON, ids array empty, or ids array exceeds the tier's per-call limit (100 for self-serve; 500 for Sync after carrefour#55).
401 missing_api_key
No X-API-Key header on a data endpoint. Every /api/v1 data endpoint requires a key; only GET /api/v1 (the root index) and /api/v1/openapi.json (the spec) are public.
401 invalid_api_key
The provided key doesn't match an active row. Could mean: wrong key, key was deleted, key was never issued. Check the prefix (first 12 chars) matches what your dashboard shows.
401 revoked_api_key
The key existed but is now revoked (explicit revocation) or rotated (replaced by a newer key). Use the current active key from your dashboard.
402 payment_required
Either your customer status is past_due (within the 7-day grace window after a failed invoice) or your key was suspended after the grace expired. Update your payment method via the Stripe Customer Portal; a successful payment immediately restores access.
404 master_not_found
The requested master_id doesn't exist in our catalogue. We return the master_id in the response body so your client can correlate.
413 payload_too_large
The request body exceeded the documented cap:
/api/v1/masters/batch: 32 KB max/api/v1/stripe/webhook: 64 KB max (you shouldn't be hitting this; webhooks come from Stripe)
429 rate_limited
You exceeded one of: per-minute burst, per-month quota, or per-key concurrency cap. Response includes:
Retry-After: <seconds>
X-RateLimit-Limit: <tier_burst>
X-RateLimit-Remaining: 0
X-RateLimit-Reset: <unix_epoch>
Body:
{ "error": "rate_limited", "retry_after_seconds": <seconds> }
Fix: respect Retry-After. Don't retry sooner than that; you'll just keep getting 429s. See rate limits for the full per-tier matrix.
5xx — our request
500 internal
Unexpected server error. We've logged it on our side; if it persists, file an issue with the X-Search-Latency-Ms header value + timestamp.
503 pool_exhausted
Database connection pool exhausted (current 144 in-flight cap). Response includes Retry-After: 5. Back off + retry.
504 request_aborted
The 15-second wall-clock deadline fired — either the client disconnected (req.signal.aborted) or a downstream query hung.
504 query_timeout
PostgreSQL statement_timeout (10s) fired. Usually means an unusually-broad query (broad facets + q-text with no narrowing). Try adding a narrower constraint.
Permanent vs transient
| Code | Transient? | Backoff strategy |
|---|---|---|
| 400 | No | Fix the request; don't retry |
| 401 / 402 | No (until you fix auth/payment) | Don't retry; fix the underlying issue |
| 404 | No | Don't retry |
| 413 | No | Reduce payload size |
| 429 | Yes | Wait Retry-After, then retry |
| 500 | Sometimes | Retry once after 5s; if persists, file an issue |
| 503 | Yes | Retry after 5s; exponential backoff if persistent |
| 504 | Yes | Retry once; if persists, narrow the query |
Standard exponential backoff with jitter: start at the documented Retry-After, double on each subsequent 429/503/504, cap at 60s.