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.

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

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

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:

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

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/search, POST /v1/events, GET /v1/theme, GET /v1/catalog/serve (the unscored read), and GET /v1/personas (the archetype gallery) are airline-scoped, not passenger-session-scoped. They take no X-Session-Id header. Sending one is harmless; it is simply 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. 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+ to the child set). Never R / NC-17 / TV-MA.
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.

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.