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.
POST /discovery/v1/search answers a shopper’s query. It serves both the as-you-type panel
and the results page, usually from the same surface.
Request
Section titled “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": "blue cotton shirt", "size": 24, "filters": { "availability": { "eq": 1 }, "price": { "lt": 50 } }, "sort": { "field": "price", "direction": "asc" } }'channel_id and surface_id are query parameters. Everything else goes in the JSON body.
| Query parameter | Required | Notes |
|---|---|---|
channel_id | Yes | Serving destination |
surface_id | Yes | Must be a search surface |
| Body field | Required | Default | Notes |
|---|---|---|---|
type | Yes | — | product, category, brand, article, query, … |
touchpoint | No | none | Where the results appear. One of autocomplete, serp, chat, mcp_agent — see Touchpoints |
query | No | "" | The shopper’s input. Up to 256 characters. |
size | No | 10 | 1–100 |
filters | No | none | Filter tree |
sort | No | relevance | { "field": …, "direction": "asc" | "desc" } — see sorting |
cursor | No | first page | From a previous next_page_cursor. Omit it for the first page; an empty string is a 422. |
return_fields | No | surface default | Field projection |
user_id | No | anonymous | Signed-in shopper |
personalize | No | true | Personalize this request |
Unknown body fields are rejected with a 422.
An empty query is valid: the surface answers with whatever it is configured to show for
no input. To browse a category instead, use /collection, which
names a collection rather than a query.
Query interpretation
Section titled “Query interpretation”A search surface can recognize brands, categories, colours and conditions in the query. Recognized values either restrict the results or boost matching products, depending on the surface configuration. A boost keeps other products eligible. An explicit request filter on the same field takes precedence over the inferred facet.
A surface can also recognize a query that is a product code, such as an EAN or a SKU. When
products carry exactly that code, the results keep only the products whose code or text matches
the query; products that only resemble the query are left out. The products whose code matches
come first, ordered by how strongly it matches, and the others follow in their usual order. A
bundle whose code lists the queried code among its parts matches as well, and can come before the
product itself. On a surface that collapses variants,
a matching product can be represented by another member of its group.
The surface’s own ranking preferences and
business rules apply on top of this order: either
can move a matching product down or lift another product above it, and a pin lifts its product to
its position. A sponsored placement can also take a slot above
the matches.
With an explicit sort, the sort order applies. A query that matches no code whole, such as the first
characters of one, is searched as usual.
Price expressions can become numeric filters on the surface’s configured price field.
The price grammar supports Bulgarian (bg), Czech (cs), German (de), Greek (el), English (en), Spanish (es), Finnish (fi), French (fr), Croatian (hr), Hungarian (hu), Italian (it), Dutch (nl), Polish (pl), Portuguese (pt, Portugal), Romanian (ro), Slovak (sk), Slovenian (sl), Swedish (sv) and Ukrainian (uk). Regional locale tags such as hu_HU use
the base language. Facet matching uses the aliases configured for that
surface, independently of the price grammar.
Hungarian examples:
| Query | Price constraint |
|---|---|
lámpa 30 000 Ft alatt | At most 30,000 |
lámpa 5000 Ft-tól | At least 5,000 |
lámpa 5000 és 10 000 Ft között | From 5,000 to 10,000, inclusive |
lámpa max. 20 ezer alatt | At most 20,000 |
The numbers use the units stored in the configured price field; query parsing does not
convert currencies. A surface can require a currency marker for ambiguous phrases and
ranges. With that setting, 5000-10000 and 5000-ig need a currency marker, while an
unambiguous price phrase such as 30 000 alatt can still be recognized. Without that
setting, bare numbers in those phrases can also become price constraints. Units can
distinguish other quantities: 40 cm alatt does not express a price.
Finnish, Portuguese and Slovenian examples use euros:
| Language | Query | Price constraint |
|---|---|---|
| Finnish | lamppu 100 euroon asti | At most 100 |
| Finnish | lamppu 100 eurosta alkaen | At least 100 |
| Portuguese | candeeiro até 100 euros | At most 100 |
| Portuguese | candeeiro entre 50 e 100 euros | From 50 to 100, inclusive |
| Slovenian | svetilka do 100 evrov | At most 100 |
| Slovenian | svetilka od 50 do 100 evrov | From 50 to 100, inclusive |
These languages recognize decimal commas, €, EUR and local euro word forms.
General comparisons and ranges need a currency marker when the surface requires one,
so vähintään 16 GB, pelo menos 16 GB and vsaj 16 GB remain product specifications.
Explicit price phrases such as hintaan alle 100, preço até 100 and po ceni do 100
can still be recognized without a currency. A model number before a trailing price can
be mistaken for thousands grouping: iphone 13 300 euroon asti reads as a maximum of
13,300. iphone 13 alle 300 euroa keeps the model number and caps at 300.
The surface also controls whether applied hard-facet and price phrases are removed from the query used for text matching. Soft boosts retain their matched text. These features must be configured on the selected surface; sending a query alone does not enable them. See Filters for the numeric operators and explicit filter syntax.
Greek examples use euros, and Swedish examples use Swedish kronor:
| Language | Query | Price constraint |
|---|---|---|
| Greek | λάμπα κάτω από 100 ευρώ | At most 100 |
| Greek | λάμπα μεταξύ 50 και 100 ευρώ | From 50 to 100, inclusive |
| Swedish | lampa under 100 kr | At most 100 |
| Swedish | lampa från 50 till 100 kronor | From 50 to 100, inclusive |
Greek recognizes €, EUR, ευρώ and unaccented spellings. Swedish recognizes
kr, kr., SEK, krona and kronor, with decimal commas and grouped thousands.
When a currency is required, τουλάχιστον 16 GB, minst 16 GB and
från 3 till 6 år remain specifications or ages. Explicit price phrases such as
τιμή έως 100 and pris under 100 can still be recognized without a currency.
Greek phrases use Greek script; Latin transliterations are not recognized.
Response
Section titled “Response”{ "hits": [ { "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt", "price": 29.9, "list_price": 39.9, "availability": 1, "image_url": "https://cdn.example.com/products/sku-1001.jpg", "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"}| Field | Meaning |
|---|---|
hits | The ranked results, best first. Flat objects; @id and @type always present. |
total | Exact count, or null when the result set is too large to count exactly |
total_approx | Always present; the fallback when total is null |
request_key | Identifies this result set — pass it to /facets |
guid | Deprecated alias of request_key, same value — see the overview |
next_page_cursor | Pass back as cursor for the next page, or null |
pagination_status | Why there is or is not a cursor — see pagination |
Autocomplete
Section titled “Autocomplete”Autocomplete is not a separate endpoint. It is the same search surface, called with a
partial query and a small size, typically once per type you want to show:
const url = `${API}/discovery/v1/search?channel_id=${channelId}&surface_id=lbs_search_main`;
const [products, categories, suggestions] = await Promise.all( ['product', 'category', 'query'].map((type) => fetch(url, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ type, touchpoint: 'autocomplete', query: input.value, size: 6 }), }).then((r) => r.json()), ),);A short query is matched against the start of words, so a word the shopper is still typing
already finds products. What counts as short is set on each surface, as a maximum number of
words and a maximum length for the last one. A single word with a hyphen between letters is
the exception when the part after the last hyphen has three or more letters, such as
tt-play or cd-dab: it’s read as a finished model name and matched as whole words only,
so e-bik isn’t taken as the start of e-bike. A shorter last part, as in hi-fi, follows
the usual rule.
Recommended client behaviour:
- Debounce, do not throttle. Wait about 120 ms after the last keystroke. A request per keystroke spends rate-limit budget on queries that are never shown.
- Abort superseded requests. Keep an
AbortControllerper input and cancel the previous request when a new keystroke arrives, so a slow early response cannot overwrite a fast later one. - Mint the token before the first keystroke. Fetching a token on the first keystroke delays the first response.
When the panel and the results page share a surface, the top autocomplete result is the top result on the results page, and retuning one retunes both.
A surface can be configured to complete the last word of a multi-word query. Every earlier
word must then match, the same way as in a full search, and the last word matches as the
start of a word or as a whole word: running sho returns running shoes and running shorts,
not everything that mentions running or has a word starting with sho. A word the
catalog’s language ignores, such as the or for in English, does not narrow the results.
Without this setting, a short partial query matches products containing any of its words
as a prefix.
Filtering as the shopper narrows
Section titled “Filtering as the shopper narrows”Facet selections become a filters tree. Rebuild it from the selected values on each
request rather than accumulating fragments:
function buildFilters({ brands, maxPrice, inStockOnly }) { const clauses = []; if (brands.length) clauses.push({ brand: { in: brands } }); if (maxPrice != null) clauses.push({ price: { lte: maxPrice } }); if (inStockOnly) clauses.push({ availability: { eq: 1 } });
if (clauses.length === 0) return undefined; if (clauses.length === 1) return clauses[0]; return { $and: clauses };}Omit filters entirely when nothing is selected — {} is not a valid filter node. Each
new filter combination is a new result set with a new request_key, so re-request facets after
the search returns. See Facets.
Surfaces configured with automatic query filters can read price bounds from the shopper’s
query. German, French and Italian surfaces recognize Swiss francs (CHF, Fr., SFr.
and local currency names). For example, lautsprecher bis CHF100 applies a maximum of
100 to the configured price field. Bounds use the numeric values stored in the catalog;
currencies aren’t converted.
When query autofilters are enabled, recognized unit abbreviations keep appliance
specifications without an explicit currency out of inferred price filters. For example, speaker under 100 W under €200 applies a price ceiling of 200 and keeps under 100 W in the query.
Grouped quantities and unit-bearing ranges also stay in the query: under 12 500 W
doesn’t set a price ceiling, and from 50 to 100 W doesn’t set a price floor.
Units include W, kW, L, cm, kg, dB, °C, м², and м³. An explicit currency still names a price, as in under €200 W speakers.
Zero results
Section titled “Zero results”An empty hits array with total: 0 is a normal response, not an error. The
no-results reports list the queries your
catalog cannot answer.
A common recovery is to fall back to a recommender surface — bestsellers, or items related to the shopper’s session — so the page still has something to offer.
Reporting what happened
Section titled “Reporting what happened”The slate a surface returned is recorded when the request is served. You report what the
shopper did next: an interaction when they click a result or add it to the cart, and a
transaction when they buy. Each of those events names
the object with a reference, which is what lets Luigi’s Box credit
it to this search. See Sending events.
See also
Section titled “See also”- Filters · Facets
- Sorting and pagination · Selecting fields
- Product variants — collapsing size and colour duplicates
- Personalization
Was this page helpful?
Thanks.