Ads Decisioning
POST /v1/ads/decision — per-member, age-gated pre-roll ad selection. No pricing, no bidding.
POST /v1/ads/decision picks one ad (plus a runner-up) to show a member before a title plays,
scored against their viewing context and age tier. There is no bid/CPM auction on this
platform — ad selection is a content-fit match, never a monetary computation.
Session-scoped — requires the X-Session-Id
header. Body (max 64 KiB):
alternativeAd— a same-shape runner-up ad, omitted wheneverselectedAdis omitted or fewer than two ads were scored. Not shown in the example above (only present when a runner-up exists); a real response can carry it alongsideselectedAd.poster— an optional ad-creative image URL, omitted when the ad has none.offeris marketing copy, never a computed price. It may contain discount language as prose ("15% off…"), but it is catalog text, not a structured monetary field — there is no price/bid/CPM field anywhere onselectedAd/alternativeAd.targetPersonasrides on the wire but is not currently used as a ranking or filter signal — it’s reserved for a future use, always present (may be[]).maxViewsPerSessionis omitted when the ad has no override; absence means the platform default (2 views per session) applies.
Three distinct “no strong pick” outcomes
Not every non-ideal outcome looks the same — distinguish them by reasoning, not just by
whether selectedAd is present:
- Pool empty (honest-null, cost-free). If age-gating, the view cap, and the cooldown window
leave zero eligible ads, the platform makes no model call and returns
selectedAdomitted,matchScore: 0,reasoning: "No eligible ads remained after the age gate, view cap, and cooldown filters." - Below threshold (honest-null, after scoring). The model did score the eligible pool, but
the top result scored under the match threshold. Same shape:
selectedAdomitted,matchScore: 0,reasoning: "No ad cleared the match threshold for this passenger." - Fallback (a real pick, not a null). If the model call itself fails after the request was
already billed (an output-guard refusal or unparseable response), the platform falls back to
a deterministic pick from the eligible pool rather than returning nothing —
selectedAdis present,matchScoreis pinned to the match-threshold floor, andreasoning: "fallback". Treat a"fallback"reasoning as “we showed something safe,” not as a genuine relevance judgment.
Age-gating
Ads reuse the platform’s age-tier model (Adult / Tween /
Child / Unknown, with Unknown resolving to Child). Each ad carries excludeChildren; when
true, both Child and Tween are excluded, not just Child. An excluded ad is dropped before
scoring, same as the pool-empty case above if nothing survives.
Errors
Standard envelope: 422 request.validation (blank seatId), 400 session.unresolved, 401 / 403, 413 request.malformed (body over 64 KiB), 500. There is no
429 for this endpoint today — a policy/ceiling denial surfaces as 500 internal.error, same as
a genuine fault.