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/collections | A listing page | A scope, such as a category |
POST /discovery/v1/recommender | Recommendations | Anchor objects, or nothing |
POST /discovery/v1/facets | Filters for a result set | The guid 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", "query": "running shoes", "size": 24 }'Three inputs say where the request is being served, and they are on nearly every call:
| 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, … |
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.
type is required and singular. 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 /collections 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, "guid": "3ab549eb64a20a0c", "next_page_cursor": "eyJvIjoyNCwic…", "pagination_status": "more"}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.
Two totals
Section titled “Two totals”| 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 guid
Section titled “The guid”Every ranked response carries a guid identifying that result set. 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.
A typical results page
Section titled “A typical results page”POST /searchwith the query → hits,total,guid.POST /facetswith thatguid→ the filter sidebar.- Shopper picks a filter →
POST /searchagain withfilters, producing a newguid. - 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 · Collections · Recommendations
- Facets · Filters · Object lookup
- Sorting and pagination · Selecting fields
- Personalization · Product variants
- Business rules — shaping results by hand
Was this page helpful?
Thanks.