--- title: Recommendations description: Fetch recommendations with POST /discovery/v1/recommender — anchors, deduplication, and the placements a recommender surface serves. slug: discovery/recommendations docKind: guide hub: luigisbox-ai --- 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 ```bash curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/recommender?channel_id=lbn_4hj9tv&surface_id=lbs_recommender_pdp_similar' \ -H 'Authorization: Bearer ' \ -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](/discovery/filters/) | | `dedup_options` | No | none | Up to 20 deduplication hints | | `return_fields` | No | surface default | Up to 100 fields — see [Selecting fields](/discovery/fields/) | | `user_id` / `personalize` | No | anonymous / `true` | [Personalization](/discovery/personalization/) | Anchors are full typed identities, exactly as they appear in `@id`, in one array: ```json { "anchor_ids": ["product/sku-1001", "product/sku-2087"] } ``` There is no `sort` and no `cursor`: a recommender returns one fixed-size slate. ## Response ```json { "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 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 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. ```bash curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/recommender?channel_id=lbn_4hj9tv&surface_id=lbs_recommender_homepage' \ -H 'Authorization: Bearer ' \ -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 `filters` applies to recommendations as on the other endpoints. Two common uses: ```jsonc // 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. ```js 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 `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](/discovery/variants/). ## 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](/analytics/sending-events/). ## See also - [Discovery overview](/discovery/overview/) · [Filters](/discovery/filters/) - [Personalization](/discovery/personalization/) · [Selecting fields](/discovery/fields/) - [Product variants](/discovery/variants/) - [Sending events](/analytics/sending-events/)