Errors & Limits

The ApiError envelope, error codes, correlation IDs, request limits, and rate limiting.

Every non-2xx response from the API carries the same machine-readable envelope, every request gets a correlation ID you can quote to support, and the request limits are fixed and documented. This page is the reference for all three.

The ApiError envelope

1{
2 "schemaVersion": "1",
3 "code": "request.validation",
4 "message": "topK must be between 1 and 100.",
5 "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d"
6}
  • code is the stable, machine-readable value to branch on — codes are additive-only and never renamed.
  • message is human-readable and actionable. It never leaks internals (no stack traces, no provider errors).
  • correlationId always echoes the request’s correlation ID.
  • ruleId appears only when a policy rule denied the request (it names the rule). Like every null field, it is omitted otherwise — responses never carry a field set to null.

Error codes

CodeHTTPMeaning
auth.invalid_token401Missing, malformed, expired, bad-signature, or wrong-issuer token — or a token that is not an access token
auth.insufficient_scope403Token valid, but the westiq/search scope is absent
auth.unknown_client403Token valid, but the client has no airline mapping (fail-closed — never a default tenant)
request.malformed400 / 413Body could not be accepted: not JSON, unparseable, empty (400) — or over the body-size cap (413)
request.validation422*Body parsed but violates a rule (query length, topK range, batch size, {"events": null})
session.unresolved400A session-scoped endpoint was called without a resolvable passenger session — a missing or key-unsafe X-Session-Id header (or {id} path segment on the SSE routes). See Sessions & Member Locators
content.refused422The two natural-language search endpoints only (/v1/search, /v1/shopping/search) — the query itself was refused on safety or scope grounds. The message is generic by design and no rule identifier is disclosed. Not retryable: rephrase the query. See content safety
rate.limited429An AI-backed or partner endpoint was called over your per-client request-rate limit. The Retry-After response header says how many seconds to back off. See Rate limits below
ingest.batch_too_large422POST /v1/catalog/ingest/jobs and POST /v1/products/ingest/jobs — more than 500 items in one batch. Split it and resubmit; the batch is never partially ingested
ingest.mixed_currency422POST /v1/products/ingest/jobs only — the batch carries two or more well-formed currencies. Rejected whole, never converted; split by currency and resubmit
ingest.job_not_found404The two job-status routes (catalog, products) — no such job for your airline on this surface. Airline-scoped, so another tenant’s job id is simply not found — and each surface reads only its own jobs, so a catalog job id polled on the product route is a 404 too
flight.not_found404PUT /v1/flights/{id}/active-catalog — no flight is registered under that id. Register it first
theme.not_found404GET /v1/theme — no theme is configured for your airline. Fail-loud, never a default/fallback theme
internal.error500Unexpected failure. The message is generic — the correlationId is the debugging handle

Unmatched routes return 401, by design: the API denies by default, so a typo’d path looks like an auth failure rather than a 404. If a request 401s unexpectedly, check the path first.

* One exception: POST /v1/destination/weather returns request.validation as a 400, not a 422, when none of airport, city, or a lat+lon pair is supplied. Every other endpoint on the platform pairs this code with 422 — branch on the code field, not the status, if you handle this generically.

Two different 422s on the search endpoints. A 422 request.validation means your request broke a rule — fix the field and retry. A 422 content.refused means the query text was refused on safety/scope grounds — show the response’s message to the passenger and don’t retry the same query. Branch on code, never on the 422 status alone.

ingest.* is mostly a per-item vocabulary, not an HTTP one. Codes like ingest.unknown_kind, ingest.missing_price, or ingest.ambiguous_match appear inside an ingest job’s report (catalog, products), one per rejected item, and never as the code of an HTTP error. The three exceptions are in the table above: ingest.batch_too_large, ingest.mixed_currency, and ingest.job_not_found. Everything else that fails at the HTTP level on these routes — including an over-cap externalTitleIds list on the flights push — is the ordinary request.validation.

Correlation IDs

Every request is assigned a correlation ID of the form req_ followed by exactly 32 hex characters. You can supply your own on the X-Correlation-Id request header — if it matches that format it is honored verbatim; anything else (or no header) is replaced with a freshly minted ID. A bad value is never a rejection.

The established ID is echoed back on the X-Correlation-Id response header and inside every error body. Log it, and quote it in support requests — it is how we find your request in our logs, and for a 500 internal.error it is the only handle there is.

Request limits

LimitValueOver the limit
/v1/search body size64 KiB413 request.malformed
/v1/events body size2 MiB413 request.malformed
Catalog-ingest body size8 MiB413 request.malformed
Product-ingest body size8 MiB413 request.malformed
Flight context body size64 KiB413 request.malformed
Active-catalog push body size2 MiB413 request.malformed
query length1000 characters422 request.validation
topK range1–100422 request.validation
Events per batch1–500422 request.validation
Titles per ingest batch1–500422 ingest.batch_too_large
Products per ingest batch1–500422 ingest.batch_too_large
Ids per active-catalog push1–10,000422 request.validation

Rate limits

Independent layers protect the platform, tied to different identities: two per-client-credential layers (AI-backed endpoints and partner endpoints, each split into their own classes) and one per-IP layer at the edge.

Per client credential, on AI-backed endpoints (429)

Every AI-backed /v1 endpoint enforces a request-rate limit per client credential, per endpoint class: search, shopping search, shopping curate & rescore, suggestions, persona generation, enrichment, ads decisioning, and destination plan/extend and search/replace. The model-free endpoints — healthz, theme, catalog serve, events, session routes, destination weather, and hero images — are not rate-limited at this layer. The partner endpoints are model-free but are limited, in their own classes — see below.

Over the limit, the endpoint returns 429 rate.limited in the standard ApiError envelope (generic message, correlation ID echoed) with a Retry-After response header giving the back-off in seconds. Handle it by pausing requests to that endpoint class for Retry-After seconds, then resuming. Three properties to build against:

  • Limits are per endpoint class — one endpoint class hitting its limit does not block the others (a search burst never starves your event ingestion, which isn’t limited at all).
  • The numeric limit is deployment-tuned, not a contract. Don’t hard-code a requests-per-minute figure — honor Retry-After instead. Well-behaved seatback traffic does not hit this limit; retry storms and hammering loops do.
  • A 429 is not a fault. Log it, back off, resume. Treat only repeated 429s after honoring Retry-After as a support conversation (quote the correlationId).

Per client credential, on the partner endpoints (429)

The partner surfaces — catalog-ingest and product-ingest job submission, each surface’s job-status polling, and flight registration and the active-catalog push — carry the same kind of per-client limit, each as its own class, with the same 429 rate.limited + Retry-After behavior.

They are deliberately generous: sized so that legitimate operational bursts — a boarding wave’s registrations and retries, or a full-catalog resync submitting a batch train while sweeping every job’s status — never see a 429.

Status polling has its own budget. Job submission and job-status polling are separate classes with separate windows, so a submission burst can never blind your status polling, and a status sweep can never consume your submission headroom. You do not need a special polling cadence to protect your submits — though a job that takes minutes doesn’t need second-resolution progress either; a few seconds between polls of the same job is plenty.

On the two ingest-submission POSTs (/v1/catalog/ingest/jobs, /v1/products/ingest/jobs), a 429 has two causes with two different bodies — the usage ceiling (ceiling-exceeded body, with resetAtUtc) and the per-client request rate (this envelope, with Retry-After). Branch on the body shape, not the status code.

Per IP, at the edge (bare 403)

The platform edge (WAF) applies a per-IP limit of 2000 requests per 5-minute window. Requests over the limit are blocked at the edge with a plain 403without the ApiError envelope. That is how you tell the two 403s apart:

  • 403 with a JSON body and a code — an API authorization failure (auth.insufficient_scope / auth.unknown_client). Fix the token or credential; retrying won’t help.
  • Bare 403 with no envelope — the edge rate limit. It clears on its own once your request rate drops below the limit. Back off and retry with jitter.

Two layers of “rejected” on /v1/events

/v1/events has a second, per-event rejection layer that is not an HTTP error: individually invalid events come back inside the 200 response’s rejected[] array with their own code vocabulary (event.*), while the ApiError envelope covers batch-level failures only. The split — and the five per-event codes — are covered in the behavioral events guide.