Errors & Limits
Operation errors use a machine-readable envelope, with correlation IDs you can quote to support. The prepared Completed Update Delivery contract is disabled by default and not available on deployed endpoints: it additionally defines empty routing 404/405 responses and a delivery-specific null-field rule, described below. This page covers the shared envelope, error codes and limits.
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.- For the existing endpoints described by this shared envelope,
ruleIdappears only when a policy rule denied the request and is omitted otherwise. The completed-delivery contract instead always includesruleId, using explicitnullwhen no rule applies. availableFrom(a date,YYYY-MM-DD) appears only withintegration.not_enabled, and only when a date is planned for that endpoint. It is otherwise absent, nevernull.
Completed-delivery routing and operation errors
Completed delivery remains unavailable until separately activated. Its operation
refusals use the shared field names with explicit nullable ruleId; see its
status/code table. Unknown or malformed
delivery paths produce empty 404, and unsupported methods on exact known paths
produce empty 405 with Allow, before authentication or body processing. These
routing responses have no
error envelope. Existing unrelated routes retain their behavior. A delivery timeout
or disconnect never proves rollback; preserve request identity when resolving ambiguity.
Error codes
Without a token, unmatched routes return 401, by design: the API denies
by default, so a typo’d path looks like an auth failure. With a valid token,
an unmatched route returns an empty 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 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, until the rate drops. The edge sends
the same 403 for other causes too: see The edge 403.
The edge 403
The platform edge sits in front of the API and can refuse a request before the
API sees it, with a plain 403 whose body is HTML (text/html), not JSON: no
ApiError envelope and no code. Among other causes, the edge sends this 403
for:
- unauthenticated traffic (no
Bearertoken) without aUser-Agentheader or with a body over 8 KB; - a call outside
/v1, such asGET /healthzor the API document atGET /openapi/v1.json, without aUser-Agent, even with a token; - a request rate over the per-IP limit of 2000 requests per 5-minute window.
Authenticated /v1 calls are not refused for a missing User-Agent or a body
over 8 KB; the API’s own request limits apply to them. We
recommend a User-Agent on every call, for example
User-Agent: your-integration/1.0.
That is how you tell the 403s apart:
- 403 with a JSON body and a
code— an API answer:auth.insufficient_scopeorauth.unknown_client(fix the token or credential), orintegration.not_enabled(the endpoint is not enabled for your integration). Retrying won’t help. - 403 with an HTML body and no envelope — the edge. If the request
carries your token and a
User-Agent, the likely cause is the rate limit: it clears on its own once your request rate drops below the limit, so back off and retry with jitter. If either is missing, add it and retry.
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.