Skip to navigation

Errors & Limits

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

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

{
"schemaVersion": "1",
"code": "request.validation",
"message": "topK must be between 1 and 100.",
"correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d"
}
  • 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.
  • For the existing endpoints described by this shared envelope, ruleId appears only when a policy rule denied the request and is omitted otherwise. The completed-delivery contract instead always includes ruleId, using explicit null when no rule applies.
  • availableFrom (a date, YYYY-MM-DD) appears only with integration.not_enabled, and only when a date is planned for that endpoint. It is otherwise absent, never null.

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

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)
integration.not_enabled403This capability is not yet enabled for your integration. The credential is valid and the endpoints enabled for it keep working; repeating the call will not help until this one is enabled. When a date is planned, availableFrom carries it
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.invalid_artwork_urlper-item reportCatalog or product artwork contains an empty, relative, non-HTTPS, over-2,048-character, whitespace-containing, or hostless URL. The bad item is rejected; otherwise-valid URLs are not checked for reachability.
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

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

LimitValueOver the limit
/v1/search body size64 KiB413 request.malformed
/v1/events body size2 MiB413 request.malformed
Scored serve body size16 KiB413 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 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 Bearer token) without a User-Agent header or with a body over 8 KB;
  • a call outside /v1, such as GET /healthz or the API document at GET /openapi/v1.json, without a User-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_scope or auth.unknown_client (fix the token or credential), or integration.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.