Errors & Limits
Errors & Limits
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
codeis the stable, machine-readable value to branch on — codes are additive-only and never renamed.messageis human-readable and actionable. It never leaks internals (no stack traces, no provider errors).correlationIdalways echoes the request’s correlation ID.ruleIdappears 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 tonull.
Error codes
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
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-Afterinstead. 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-Afteras a support conversation (quote thecorrelationId).
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 403 — without 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.