Shopping Curate & Rescore
Shopping Curate & Rescore
POST /v1/shopping/curate and POST /v1/shopping/rescore — sectioned, per-member shopping with code-owned pricing.
Two calls drive the shopping surface for one member:
POST /v1/shopping/curate— build a fresh, sectioned set of picks from the full catalog.POST /v1/shopping/rescore— re-score a set of products you’re already showing (e.g. as new behavioral signal arrives), without re-sectioning from scratch.
Both are session-scoped — require the
X-Session-Id header.
No price field exists on either request, anywhere. Pricing is always catalog-sourced and attached server-side after scoring — there is nowhere on the wire for a client (or a model) to assert a price. If you send one anyway, it is silently dropped during parsing; it never reaches the service.
Curate
Body (max 4 KiB):
totalCandidatesandageExcludedCountare computed against your airline’s entire catalog — curate has no category/price pre-filter the way search does.ageTierAppliedis the resolved age tier (Unknownbehaves asChild).personaBoundis the same cold-start signal used elsewhere:falsemeans no persona/affinity is bound yet, so section ordering is affinity-neutral rather than fabricated.
Age-gating happens before the model ever sees a candidate, not after. The catalog is hard
age-gated in code first; an excluded product is never placed in the pool the model scores from,
and the gate is re-applied to whatever comes back as defense-in-depth. matchScore (0–100) is
always code-clamped regardless of what was asserted internally.
Rescore
Body (max 16 KiB):
sectionUpdates is partial — only sections whose product set or scores actually changed are
present; regroup items into their existing section id, don’t replace the whole layout.
promotedCount/demotedCount count items whose score moved by 8 or more since their last-known
value.
Rescore re-gates the visible set before any model call — client-flagged “visible” items can be
silently dropped. The server re-resolves the member’s age tier from scratch (never trusts the
client) and re-applies the age gate to your visibleProductIds first. An id your client
considers visible but that this member’s tier may not see is dropped and never sent to the
model at all — it simply won’t appear in sectionUpdates. Treat visibleProductIds as
advisory, not authoritative: always render from the platform’s response, not your own tracked
visible set.
A product with no prior curate/rescore state for this member is honestly dropped from the response rather than assigned a fabricated section. Two members sharing one seat get fully isolated curations — one member’s signal never influences the other’s.
Both requests accept a couple of extra fields reserved for forward compatibility —
ShoppingCurateRequest.existingSectionIds and ShoppingRescoreRequest.enrichmentCycle. Sending
them is harmless; neither currently affects the response.
Errors
Standard envelope. Both: 400 session.unresolved, 401 / 403, 500.
Curate additionally: 422 request.validation (blank seatId). Rescore additionally: 422 request.validation (blank seatId, empty visibleProductIds, or more than 80). There is no
429 for either endpoint today — a policy/ceiling denial, or an output-guard rejection of model
text, surfaces as 500 internal.error.