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.
Discovery is the read side of Luigi’s Box AI: you ask a configured surface a question and it answers with a ranked list of catalog objects. Four endpoints cover the whole surface area.
| Endpoint | Ask it for | The request supplies |
|---|---|---|
POST /discovery/v1/search | Results for a query | A search query |
POST /discovery/v1/collection | A listing page | A collection, such as a category |
POST /discovery/v1/recommender | Recommendations | Anchor objects, or nothing |
POST /discovery/v1/facets | Filters for a result set | The request_key of a previous result set |
Two more endpoints support the first four: object lookup fetches objects by reference without ranking them, and variant resolution expands a product into its group.
Every discovery endpoint is a POST that carries its inputs as a JSON body. Filters and
sort are structured JSON objects.
Anatomy of a request
Section titled “Anatomy of a request”curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/search?channel_id=lbn_4hj9tv&surface_id=lbs_search_main' \ -H 'Authorization: Bearer <token>' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -H 'Content-Type: application/json' \ -d '{ "type": "product", "touchpoint": "serp", "query": "running shoes", "size": 24 }'Four inputs say where the request is being served. The first three are on nearly every call; touchpoint is optional:
| Input | Where | What it names |
|---|---|---|
channel_id | Query parameter | The serving destination — which storefront or market this is |
surface_id | Query parameter | The configured discovery unit answering the request |
type | Body field | Which kind of object you want back: product, category, brand, … On a collection request the collection_id names it instead |
touchpoint | Body field | Where in the shopper’s journey the results appear: serp, plp, basket, … Optional, but send it on every request to /search, /collection and /recommender; each accepts only the touchpoints its intent serves |
channel_id and surface_id are query parameters; everything the surface is asked goes
in the JSON body. channel_id names the resource the request is authorized on, so leaving
it out is a 400 rather than a 422.
The surface’s intent has to match the endpoint. lbs_search_main belongs to /search;
sending it to /recommender is rejected. See
Surface identifiers.
On search and recommendation requests type is required and singular; a collection request
takes no type, since its collection_id names the member type. An autocomplete panel showing products, categories and
suggested phrases issues three requests, one per type. Each type gets its own ranking and
its own result count.
Body fields every ranked endpoint shares
Section titled “Body fields every ranked endpoint shares”| Field | Default | Purpose |
|---|---|---|
filters | none | Restrict results — see Filters |
return_fields | surface default | Choose which fields come back — see Selecting fields |
size | 10 | How many results. Max 100. |
user_id | anonymous | The signed-in shopper — see Personalization |
personalize | true | Whether to personalize this request |
An unrecognised body field is a 422.
/search and /collection additionally take sort and cursor; see
Sorting and pagination.
Required headers
Section titled “Required headers”| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <token> | Yes | A token for the …/discovery audience |
Content-Type: application/json | Yes | Every discovery endpoint takes a JSON body |
X-Lbx-Visitor-Id | Yes, except on /facets | Identifies the browser |
User-Agent, Referer | Optional | Improve traffic classification and reporting |
A request without X-Lbx-Visitor-Id is rejected, except on /facets, which re-reads a
result set the visitor was already identified for. Generate the identifier once per browser
and keep it stable. See
Identifying the shopper.
Anatomy of a response
Section titled “Anatomy of a response”{ "hits": [ { "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt", "price": 29.9, "url": "https://example.com/products/blue-cotton-t-shirt" } ], "total": 412, "total_approx": 412, "request_key": "3ab549eb64a20a0c", "guid": "3ab549eb64a20a0c", "next_page_cursor": "eyJvIjoyNCwic…", "pagination_status": "more"}That is the shape /search and /collection answer with. A recommender answers with
hits alone — one fixed-size slate has no counts, no request_key and no cursor. See
Recommendations.
Hits are flat
Section titled “Hits are flat”A hit is a flat JSON object. @id and @type are always present; everything else is the
catalog fields the surface returns, under the names you indexed them with. There is no
attributes wrapper and no per-field envelope:
hits.map((hit) => ({ id: hit['@id'], title: hit['@title'], price: hit.price }));Which fields appear depends on the surface’s configuration, your return_fields, and your
credentials — a browser token never sees private: fields. See
Selecting fields.
Disclosed placements
Section titled “Disclosed placements”A hit a paid campaign pinned or boosted carries a @disclosure object. A bury from the same
campaign discloses nothing, because it promotes nothing. It is a response-only field: it is
never part of your catalog, and it appears only on the products the campaign’s pins and boosts
actually placed.
{ "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt", "@disclosure": { "type": "sponsored", "advertiser": "Northwear Ltd", "payer": "Northwear Group", "recipient_parameters": ["search_query"] }}| Field | Meaning |
|---|---|
type | sponsored — the placement was paid for. |
advertiser | On whose behalf the placement is shown, as the campaign wrote it. Absent when the campaign names no one, and on an ad platform’s placement. |
payer | Who paid for it. Absent when the campaign names no payer. |
recipient_parameters | What decided that this shopper sees the placement on this request. Present on a campaign’s placement. |
provider | The ad platform that sold the placement. Absent for a campaign’s own disclosure. |
tracking | Impression and click URLs, under the ad platform’s own key names. Absent otherwise. |
Those six are the whole object; it carries no other keys. Render advertiser and payer as
text, not as markup.
recipient_parameters holds codes, which you word in the shopper’s language:
| Code | The placement is shown because of |
|---|---|
search_query | what the shopper searched for |
collection | the collection the shopper is browsing |
anchor | the products the recommendation is shown for |
none | nothing about the shopper; never combined with another code |
More codes may be added; word one you do not recognise generically rather than failing on it.
If your site is an online platform under the EU Digital Services Act, Article 26 requires it to
show, with each ad, that it is an ad, on whose behalf it is shown, who paid for it when that is
someone else, and the main parameters that decided this shopper was shown it. A campaign’s
@disclosure carries each of these when the campaign names its advertiser; an ad platform’s
placement carries only type, provider and tracking. The wording around them is yours.
Render a label whenever @disclosure is present. The field is absent — not null — on every
other hit, so a presence check is enough.
It appears only on /search, /collection
and /recommender, the endpoints that rank. The direct lookups —
bulk get, objects and variant siblings — return what your catalog holds without consulting
any campaign, so no hit of theirs is ever disclosed.
Where it comes from depends on the placement: a
business-rules campaign
declares it once and every product its rules pin or boost carries it, and an ad platform sets it
on the placements it returns, adding its own provider and tracking keys. A product
claimed by both is disclosed once, by the ad platform.
Two totals
Section titled “Two totals”/search and /collection count the result set two ways.
| Field | Meaning |
|---|---|
total | The exact count. null when the result set is too large to count exactly. |
total_approx | Always present. Use it when total is null. |
For a result count in your UI, read total ?? total_approx. If the surface collapses
variants, the count is of variant groups, not individual products — five sizes of one
shirt count once. Calling /facets on the result set upgrades
total to an exact figure.
The request_key
Section titled “The request_key”/search and /collection carry a
request_key identifying that result set: a hash of the request parameters that decide
which objects are in it, so the same request always yields the same value. A personalized
request without an explicit sort is ranked for one shopper, so its key also depends on the
shopper: two visitors sending it get different values. Every other request keys the same for
everyone. Pass it to
/facets to compute filters over the same results, and to
/variants when expanding groups from that page. It is opaque —
do not parse it.
The same value is also returned as guid. That name is deprecated: read request_key
instead, and send request_key to /facets and /variants. guid keeps carrying the
request key for now and will later change meaning to a per-request identifier, so code that
still reads it will break at that point.
/recommender carries neither. Its slate is not cached, so
there is no result set for /facets or /variants to address.
A typical results page
Section titled “A typical results page”POST /searchwith the query → hits,total,request_key.POST /facetswith thatrequest_key→ the filter sidebar.- Shopper picks a filter →
POST /searchagain withfilters, producing a newrequest_key. - Shopper scrolls →
POST /searchagain withcursorfromnext_page_cursor. - Report what was shown and clicked with analytics events.
A surface learns from the interactions you report; without them its ranking does not improve.
See also
Section titled “See also”- Search · Collection · Recommendations
- Facets · Filters · Object lookup
- Sorting and pagination · Selecting fields
- Personalization · Product variants
- Business rules — shaping results by hand
Was this page helpful?
Thanks.