Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
A recommender takes anchors — the products the current context is about — and returns items related to them. With no anchors it still answers, from the session and from what is popular.
POST /discovery/v1/recommender serves every placement: the product page strip, the cart
cross-sell, the homepage carousel, the abandoned-basket email.
Request
Section titled “Request”curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/recommender?channel_id=lbn_4hj9tv&surface_id=lbs_recommender_pdp_similar' \ -H 'Authorization: Bearer <token>' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -H 'Content-Type: application/json' \ -d '{ "type": "product", "anchor_ids": ["product/sku-1001"], "size": 8 }'| Query parameter | Required | Notes |
|---|---|---|
channel_id | Yes | Serving destination |
surface_id | Yes | Must be a recommender surface |
| Body field | Required | Default | Notes |
|---|---|---|---|
type | Yes | — | Usually product |
anchor_ids | No | none | Up to 50 object identities the recommendation is based on |
size | No | 10 | 1–100 |
filters | No | none | Filter tree |
dedup_options | No | none | Up to 20 deduplication hints |
return_fields | No | surface default | Up to 100 fields — see Selecting fields |
user_id / personalize | No | anonymous / true | Personalization |
Anchors are full typed identities, exactly as they appear in @id, in one array:
{ "anchor_ids": ["product/sku-1001", "product/sku-2087"] }There is no sort and no cursor: a recommender returns one fixed-size slate.
Response
Section titled “Response”{ "hits": [ { "@id": "product/sku-2087", "@type": "product", "@title": "Blue Linen Shirt", "price": 44.9 } ], "total": 8}Recommendations are a fixed-size slate, not a browsable result set, so the response has no
guid and no cursor: there is no page two of a carousel, and there are no facets over one.
total is the number of items in the slate.
Choosing a surface per placement
Section titled “Choosing a surface per placement”Which model runs, what it considers related, and how it treats the anchors are all the surface’s configuration — not request fields. So one placement is one surface:
| Placement | Anchors you send | Surface, by example |
|---|---|---|
| Product page — similar items | The product being viewed | lbs_recommender_pdp_similar |
| Product page — bought together | The product being viewed | lbs_recommender_pdp_cross_sell |
| Cart | Every product in the cart | lbs_recommender_basket |
| Homepage | None | lbs_recommender_homepage |
| Post-purchase email | The products ordered | lbs_recommender_post_purchase |
| Empty search results | The query’s best guess, or none | lbs_recommender_zero_results |
Two placements sharing a surface share its tuning: retuning it for the homepage retunes it in the cart too. Each placement is still measured separately.
Recommending with no anchors
Section titled “Recommending with no anchors”Homepage and landing-page strips have nothing to anchor on. Send no anchor_ids and the
surface falls back to what it knows: the session’s behaviour, and what is popular in the
channel.
curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/recommender?channel_id=lbn_4hj9tv&surface_id=lbs_recommender_homepage' \ -H 'Authorization: Bearer <token>' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -H 'Content-Type: application/json' \ -d '{ "type": "product", "size": 12 }'Keep X-Lbx-Visitor-Id stable so the surface can recognise a returning shopper.
Filtering recommendations
Section titled “Filtering recommendations”filters applies to recommendations as on the other endpoints. Two common uses:
// Never recommend what cannot be bought{ "availability": { "eq": 1 } }
// Never recommend across a boundary that would confuse the shopper{ "gender": { "in": ["women", "unisex"] } }To exclude what is already in the cart, build the tree from the cart contents rather than post-filtering the response, so the slate comes back full.
const filters = { availability: { eq: 1 }, '@id': { not_in: cart.map((line) => line.id) },};Send not_in only when the cart is non-empty — an empty array is a 422.
Deduplication
Section titled “Deduplication”dedup_options carries hints about what counts as a duplicate within one slate — most
usefully, collapsing several variants of the same product into one recommendation. The
accepted values depend on your surface’s configuration; ask your Luigi’s Box contact which
apply to yours.
If your catalog uses variant groups, collapsing is usually configured on the surface instead, and needs nothing on the request. See Product variants.
Reporting
Section titled “Reporting”The slate is recorded when the request is served, including which surface answered and which anchors you sent. Report what the shopper does with it — clicks, cart additions, purchases. See Sending events.
See also
Section titled “See also”Was this page helpful?
Thanks.