Skip to navigation

Sessions & Member Locators

The X-Session-Id header, seat/member locators, and the age-tier model the personalization endpoints share.

The personalization endpoints work in terms of a passenger session and the members seated within it. Search and events don’t need any of this; the theme, catalog-serve, suggestions, enrichment, and persona endpoints do. This page defines the three shared concepts once — the per-endpoint guides link back here rather than repeat them:

  1. the X-Session-Id header that names the passenger session,
  2. the seatId / memberSlot locators that name a member within it, and
  3. the age tiers that gating is expressed in.

Completed Update Delivery is not available on deployed endpoints and is disabled by default. Its seven scoped routes use a canonical lowercase, non-nil session UUID in the URL, never X-Session-Id or seat/member fields. The session locator and receipt proof confer no authority. Delivery’s leases, counters and acknowledgements are separate from the session locators and event-stream cursors described here.

Your credential still carries your airline (see Authentication). Everything on this page is a within-tenant locator — it selects which of your own airline’s sessions, seats, and members to act on. It never crosses airlines and never elevates privilege: a request can only ever reach the airline its credential is bound to.

The X-Session-Id header

These endpoints are scoped to a single passenger session and require the X-Session-Id request header:

/v1/shopping/hero-image, /v1/destination/hero-image, and /v1/destination/weather are model-free and not in this set — see Endpoints that need no session.

The value is the seatback passenger session id — the same identifier your client puts in the sessionId field of behavioral events. It is stable for the life of one passenger’s session, so every call for that passenger — binding members, generating a persona, scoring the catalog, requesting suggestions — carries the same value and is threaded together server-side.

curl -X POST "https://api.staging.westiq.ai/v1/suggestions" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Session-Id: seat-12A-f3d9c1" \
-H "Content-Type: application/json" \
-d '{ "seatId": "12A", "titleIds": [2041] }'

The personalization endpoints identify titles by integer titleId (2041), not the prefixed "M:2041" string that search results carry — the integer is the numeric part of that id.

Value rules

The header value becomes part of a storage key, so it has the same strict shape as an event sessionId, plus one addition:

  • 1–128 characters.
  • No # (the key delimiter) and no control characters.
  • No /.
  • Must not begin with apx_ (a reserved identity prefix).

Generated ids — a UUID, or a pattern like seat-12A-<hex> — are always safe. Pre-validate in your client so a bad value never reaches the wire.

When the header is missing or invalid

The request fails closed with 400 session.unresolved — before any work is done, and never by silently falling back to a non-session default:

{
"schemaVersion": "1",
"code": "session.unresolved",
"message": "No resolvable passenger session id for this endpoint. Provide a valid X-Session-Id header.",
"correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d"
}

session.unresolved is in the standard error envelope — branch on code, not on the message text.

SSE endpoints name the session in the URL

The two server-sent-events routes are also session-scoped, but they take the session id as the {id} path segment, not the header:

GET /v1/sessions/{id}/stream
GET /v1/sessions/{id}/state

They do not read X-Session-Id. The path value follows the same key-safety rules above (the / rule is moot — the URL structure handles it), and a missing or unsafe value fails the same way, 400 session.unresolved. Both routes are covered in the session events guide.

Endpoints that need no session

POST /v1/events, GET /v1/theme, GET /v1/catalog/serve (the unscored read), GET /v1/personas (the archetype gallery), POST /v1/shopping/hero-image, POST /v1/destination/hero-image, and POST /v1/destination/weather are airline-scoped, not passenger-session-scoped — the last three are also model-free. None take an X-Session-Id header. Sending one is harmless; it is simply ignored.

The partner operations surfaces are airline-scoped for a stronger reason: they carry no passenger context at all, by construction. Catalog ingestion (POST/GET /v1/catalog/ingest/jobs…) and the flight endpoints (PUT /v1/flights/{id} and its /active-catalog) are machine-to-machine: a catalog load and a boarding-time catalog push have no passenger session to name. They take no X-Session-Id.

POST /v1/search is the one session-optional endpoint. With no X-Session-Id header it behaves exactly as an airline-scoped, sessionless search — the unchanged behavior for server-to-server callers. Send the header (with seatId / memberSlot in the body) and the search adopts the session and opts into behavior personalization. Because the header is read there, its value must be valid when present: a malformed X-Session-Id on /v1/search is a 400 session.unresolved, exactly as on the session-required endpoints — not ignored.

Seat & member locators

Within a session, a member is identified by seat and slot:

LocatorMeaning
seatIdThe physical seat — "12A". Required to identify a member; omitting it (or leaving it blank) resolves to the most-restrictive tier, never an error.
memberSlotWhich traveler on a shared-seat profile — e.g. two passengers watching on one seat. Omit it for a solo traveler; the solo member is resolved automatically.

Both are passed as query parameters on GET /v1/catalog/serve and as body fields on the scored/personalization POSTs — including POST /v1/search when you opt a search into personalization. Like X-Session-Id, they are within-tenant locators — a way to say which member, never a trust assertion.

Locators fail closed, never loud. An unknown seat, an unbound member, or a malformed/crafted locator resolves to the Unknown age tier — treated as the most restrictive (child-safe) set — and returns results accordingly. It is never a 4xx/5xx. So restricted or empty results from an unrecognized locator are the designed fail-closed behavior, not a failure to debug. Bind your members first (below) to get their real tier.

Age tiers

Gating is expressed in four tiers. A member’s tier is resolved server-side from the age band you bind at session setup; it is never sent per-request.

TierSees
AdultEverything. An adult is not restricted by this gate — including titles with a missing or unrecognized rating.
TweenUp to the PG-13-equivalent set (adds PG-13, TV-14, 13+, and the international 12 / 12+ to the child set). Never R / NC-17 / TV-MA — and the international 15 / 18 are likewise adult-visible only, above this band by design.
ChildThe most restrictive real set: G, PG, U, U/A, TV-G, TV-PG only.
UnknownThe fail-closed default for any unresolved or unbound member — treated identically to Child.

Tier membership is checked case-insensitively against the rating vocabulary; a rating outside a tier’s set — including one the vocabulary doesn’t know at all — is excluded for that tier, never guessed in.

Two rules follow from this and matter for integration:

  • Unknown age resolves to Child, not Adult. Until a member is bound with a known age band, they see the child-safe catalog. This is deliberate child-safety, not a cold-start bug.
  • A title with a missing or unrecognized rating is excluded for every non-adult tier — guessed out, never in.

To give a member their real tier, bind the seat’s roster once per session with POST /v1/personas/session, which takes each member’s seatId and ageBand and resolves an authoritative tier server-side. How each endpoint applies a resolved tier (filtering, empty responses, exclusion counts) is documented in that endpoint’s own guide.