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=1 and we didn't document foo, 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.