> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.westiq.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.westiq.ai/_mcp/server.

# Flights & Active Catalog

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 centrally** — [catalog ingestion](/guides/catalog-ingest). 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](/guides/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

```bash
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**.

| Field                   | Required | Rules                                                                              |
| ----------------------- | -------- | ---------------------------------------------------------------------------------- |
| `originIata`            | yes      | Exactly three **uppercase** letters (`"LIS"`). Otherwise `422 request.validation`. |
| `destinationIata`       | yes      | Same rule (`"EWR"`).                                                               |
| `scheduledDepartureUtc` | yes      | UTC timestamp.                                                                     |
| `scheduledArrivalUtc`   | yes      | UTC timestamp, **after** `scheduledDepartureUtc` — otherwise a 422.                |
| `aircraftType`          | no       | Free-form designation (`"A350-900"`).                                              |
| `cabinClasses`          | no       | The 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](/guides/search#time-aware) on search, for clients that don't
send `flightMinutesRemaining` themselves.

## Push the active catalog \[#active-catalog]

```bash
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"]
      }'
```

```json
{
  "schemaVersion": "1",
  "activeCount": 2,
  "unresolvedExternalIds": ["CMS-90310"]
}
```

| Field              | Required | Rules                                                                                                 |
| ------------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `sourceSystem`     | yes      | The **same** id namespace you [ingested](/guides/catalog-ingest) these titles under. Non-blank.       |
| `externalTitleIds` | yes      | 1–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.

> **Warning**
>
> **`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](/guides/catalog-ingest)
first, then push again.

Remember that an external id becomes resolvable only when the ingest batch
that submitted it **completes** — [see the ingestion guide](/guides/catalog-ingest#resolvable) for what
"unresolved" does and does not tell you.

> **Warning**
>
> **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 \[#narrowing]

Once a flight has an active set, a session **bound to that flight** (via
`flightInstanceId` at [session setup](/guides/session-setup)) has its results
intersected with that set on three surfaces:

* [Natural-language search](/guides/search) — every mode, semantic queries
  and the recommend-for-me lineup alike.
* [Scored catalog serve](/guides/catalog-serve#scored-serve) (`POST`).
* [Suggestions](/guides/suggestions).

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.

> **Note**
>
> **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](/guides/catalog-ingest)). 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](/guides/errors) 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](/guides/errors).

**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](/api-reference).