Behavioral Events
Batch passenger interaction signals into POST /v1/events — the batch model, rejection codes, idempotency, and the signal vocabulary.
POST /v1/events ingests batches of passenger interaction signals (title
clicks, watch progress, product views, …) from your seatback client. The
platform stores each event server-stamped with your airline, a derived signal
weight, and a retention TTL. This guide covers the semantics an SDK builder
needs; exact schemas are in the API Reference.
The batch model
One POST carries 1–500 events; the body is capped at 2 MiB. Batching is the norm — a seatback session produces a steady trickle of signals, and you flush them in small batches (see client cadence).
A successful batch returns 200 with the per-event tally:
Every event object carries:
What the server adds — and why the wire has no such fields
Two fields you might expect are deliberately absent from the wire:
- No airline/tenant field. The platform stamps
airline_idfrom your authenticated credential. A request cannot tag events for another airline, because there is nowhere to say so. - No weight field. Signal weights drive personalization scoring, so they are derived server-side from the weight table — a client can never manufacture engagement by declaring its own weights.
The platform also stamps identity/consent state (events are anonymous, session-scoped in the current release), the batch’s correlation id, receipt time, and a retention TTL (90 days in the sandbox).
Storage is fail-closed: each event’s data is bound to its signal type’s
schema, and only the modeled fields survive. Unknown fields, nested objects,
and arrays of objects are dropped before storage — do not rely on unmodeled
fields persisting.
Batch-level vs per-event failures
The boundary rule: once the batch envelope parses, one bad event can never fail the others. Per-event problems ride inside the 200 result; only envelope-level problems are HTTP errors.
Because one envelope-level TYPE error 400s the entire batch, validate field types client-side before sending — your SDK should make it impossible for one malformed event to take down 499 good ones.
Per-event rejection codes
Each entry in rejected[] is {"index": …, "code": …, "message": …}, where
index is the event’s position in the array you submitted (order preserved).
For the two *_error codes, the message is deliberately generic — quote the
batch’s X-Correlation-Id when reporting one and we can see the full detail
in our logs.
Idempotency & retries
Each event’s storage identity is (your airline, sessionId,
timestampMs, id). Submitting the same identity again — in the same
batch or any later one — counts in duplicates and is never stored twice.
- Partial-send recovery is “resend the whole batch.” If connectivity
drops mid-request and you can’t tell what landed, resend everything:
already-stored events tally as
duplicates, the rest store normally. - Within one batch, duplicates resolve deterministically: the lowest index claims the identity; later same-identity events report as duplicates.
- The same
idwith a differenttimestampMsorsessionIdis a different identity and stores as a new event — reuse ids only for true retries of the same event.
sessionId key safety
sessionId becomes part of the storage key, so it has a strict shape:
1–128 characters, no #, no control characters. A violating event is
rejected individually (event.malformed_payload, without echoing the value).
Pre-validate in your SDK; generated ids (UUIDs, seat-12A-<hex>, …) are
always safe.
PII rules
The events surface is built so personal data cannot leak into storage:
- Exactly one free-text field exists in the entire contract:
data.queryonsearch_query. It is PII-scrubbed and then stored — a passenger who types an email address into search does not put that address in the store. - Every other string field is an id, enum, or metric by design. If any of
them (including
seatIdandmemberSlot) carries a value shaped like personal data — an email, a loyalty number, a record locator — that event is rejected withevent.pii_shape. Rejected, never scrubbed-and-kept. - Unmodeled fields are dropped before storage (see the fail-closed note above), so there is no unguarded path into the store.
Never put names, loyalty ids, or contact details into id fields. memberSlot
selects which member of a shared profile is active (“slot-2”), it does not
identify who they are.
Client cadence
The platform enforces batch size and shape — flushing cadence is your SDK’s job. The reference client behaves like this, and we recommend the same:
- Flush the buffered batch every 2000 ms or at 5 buffered signals, whichever comes first.
- Dedup repeats of the same signal type against the same target within
500 ms (a double-tap is one click). The target is the first present of
titleId→productId→placeId→adIdindata. - Buffer offline. Seatback connectivity comes and goes: hold signals through the gap and replay them in order on reconnect. Idempotency makes replays safe even when an earlier send partially landed.
Signal vocabulary reference
50 signal types across 7 categories. Weight is what the platform assigns
server-side (higher = stronger personalization signal; watch_progress ramps
with actual progress: clamp(floor(progressPct / 15), 1, 7)). Required
fields must be present and non-null in data, or the event is rejected
with event.missing_fields; all other payload fields are optional.