Flights & Active Catalog

Register a flight’s operational context and the titles actually loaded on the aircraft — so a passenger is never shown something that isn’t there.

Two endpoints, both idempotent and both machine-to-machine (no X-Session-Id, no model call):

  • PUT /v1/flights/{flightInstanceId} — register or fully replace a flight’s operational context (route and schedule).
  • PUT /v1/flights/{flightInstanceId}/active-catalog — fully replace the set of titles actually loaded on that aircraft.

They are disjoint. Registering a flight never disturbs its active catalog, and pushing an active catalog never changes the context.

Both are optional. An airline that flies one catalog everywhere can ignore this page entirely: a session with no flight binding, and a flight with no active-catalog push, see the full airline catalog exactly as before.

The order of operations

  1. Ingest your titles centrallycatalog ingestion. The active-catalog push only resolves external ids; it never creates titles.
  2. Register the flight (PUT /v1/flights/{id}).
  3. Push the active catalog (PUT /v1/flights/{id}/active-catalog) — the flight must already be registered, or the push is a 404.
  4. Bind the session to the flight at session setup, by sending flightInstanceId.

The flight instance id

The same shape rule applies wherever a flight instance id appears — the route segment on both endpoints, and flightInstanceId on a session-setup request:

  • 1–128 characters, non-blank.
  • No control characters.
  • Otherwise free-form: a compact operational id such as "NH0007-2026-07-27", not free text.

A bad value is 422 request.validation. The message names the field and never echoes your value back.

Register a flight

$curl -X PUT "https://api.staging.westiq.ai/v1/flights/NH0007-2026-07-27" \
> -H "Authorization: Bearer $ACCESS_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{
> "originIata": "LIS",
> "destinationIata": "EWR",
> "scheduledDepartureUtc": "2026-07-27T09:40:00Z",
> "scheduledArrivalUtc": "2026-07-27T18:05:00Z",
> "aircraftType": "A350-900",
> "cabinClasses": ["economy", "business"]
> }'

Returns 204 No Content. Body cap: 64 KiB.

FieldRequiredRules
originIatayesExactly three uppercase letters ("LIS"). Otherwise 422 request.validation.
destinationIatayesSame rule ("EWR").
scheduledDepartureUtcyesUTC timestamp.
scheduledArrivalUtcyesUTC timestamp, after scheduledDepartureUtc — otherwise a 422.
aircraftTypenoFree-form designation ("A350-900").
cabinClassesnoThe cabin classes this flight operates.

Idempotent, and a full replace. Sending the same request twice yields the same stored record, so it is always safe to retry. Sending a different body replaces the record — this is not a merge, so send the complete context every time.

It carries no passenger data by construction. There is no field on this shape for one.

The scheduled arrival time is what powers the server-derived remaining flight time on search, for clients that don’t send flightMinutesRemaining themselves.

Push the active catalog

$curl -X PUT "https://api.staging.westiq.ai/v1/flights/NH0007-2026-07-27/active-catalog" \
> -H "Authorization: Bearer $ACCESS_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{
> "sourceSystem": "partner-cms",
> "externalTitleIds": ["CMS-88421", "CMS-88422", "CMS-90310"]
> }'
1{
2 "schemaVersion": "1",
3 "activeCount": 2,
4 "unresolvedExternalIds": ["CMS-90310"]
5}
FieldRequiredRules
sourceSystemyesThe same id namespace you ingested these titles under. Non-blank.
externalTitleIdsyes1–10,000 ids per request; over the cap → 422 request.validation (split the push). No blank entries.

Body cap: 2 MiB. This call is designed for the short boarding window: ids only — nothing is embedded or re-ingested here, so it is fast and retry-safe.

activeCount is not submitted − unresolved. It counts distinct titles, so it is lower than the number of ids that resolved whenever several external ids name the same title (separate ids for two encodes of one film, say). Do not build a reconciliation check on submitted == activeCount + unresolvedExternalIds.length — it is not an identity.

Unresolved ids

Ids the platform cannot resolve to a known title are returned verbatim in unresolvedExternalIds and are never guessed onto a similar title. The fix is always the same: ingest those titles centrally first, then push again.

Remember that an external id becomes resolvable only when the ingest batch that submitted it completessee the ingestion guide for what “unresolved” does and does not tell you.

If nothing resolves, the flight’s active set becomes empty. activeCount is 0, every id is echoed back, and a session bound to that flight then sees no catalog at all on the narrowed surfaces. This is fail-loud, not fail-safe: the push replaces the set with exactly the titles the submitted ids resolved to, and an empty resolution is an empty set. Check unresolvedExternalIds on every push.

What a flight binding changes: narrow-only

Once a flight has an active set, a session bound to that flight (via flightInstanceId at session setup) has its results intersected with that set on three surfaces:

This is the passenger-honest guarantee, end to end: a flight-bound session cannot be shown a title that is not loaded on the aircraft, on any of those three surfaces.

Three things it never does:

  • It never widens. The intersection can only ever narrow what the session already sees; it cannot reach outside your airline’s catalog.
  • It never overrides the age gate. An active title that is age-inappropriate for the identified member stays hidden. Narrowing and child-safety compose; neither defeats the other.
  • It never affects an unbound caller. A session with no flight binding, a flight with no push, and every sessionless machine-to-machine call keep full-catalog behavior exactly as before.

The unscored GET /v1/catalog/serve is never flight-bound. It accepts no session locator, so there is no binding for it to resolve — it always returns the full airline catalog, age-gated. Use the scored POST when you want the flight’s active set.

Counts on a narrowed request

On a flight-narrowed request, totalItems (and search’s coverage percentages) report the narrowed basis — the titles this request could actually draw from, which is your visible catalog intersected with the flight’s active set, before age-gate exclusion. ageExcludedCount counts the age gate’s removals from that same basis, so on the serve surfaces totalItems − ageExcludedCount is exactly the servable total.

Un-narrowed requests report unchanged numbers. The practical consequence: totalItems is a per-request figure, not a library size — it equals your airline catalog only when the request is not narrowed. If you display “N titles available”, that is the right number for a bound session and the wrong number to cache as a catalog statistic.

Request rate limits

Both flight routes are covered by per-client request-rate limits, each with its own window, sized like the other partner write routes (catalog-ingest job submission). The limits are deliberately generous — sized so a boarding wave’s registrations and retries never see a 429.

Over the limit, both endpoints return 429 rate.limited in the standard error envelope with a Retry-After header. Honor it and back off. The numeric limits are deployment-tuned and are never disclosed in the response.

Errors

Standard envelope.

Both endpoints: 422 request.validation (a malformed flightInstanceId, or a body that breaks a rule above), 400 request.malformed (body not JSON, or empty), 413 request.malformed (over the body cap), 401 / 403, 429 rate.limited, 500.

The active-catalog push additionally: 404 flight.not_found when no flight is registered under that id — with an actionable message telling you to register the flight first. Register, then push.

Full request and response schemas are in the API Reference.