Skip to content

    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.

    Search

    View source

    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.

    Terminal window
    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 parameterRequiredNotes
    channel_idYesServing destination
    surface_idYesMust be a search surface
    Body fieldRequiredDefaultNotes
    typeYes—product, category, brand, article, query, …
    touchpointNononeWhere the results appear. One of autocomplete, serp, chat, mcp_agent — see Touchpoints
    queryNo""The shopper’s input. Up to 256 characters.
    sizeNo101–100
    filtersNononeFilter tree
    sortNorelevance{ "field": …, "direction": "asc" | "desc" } — see sorting
    cursorNofirst pageFrom a previous next_page_cursor. Omit it for the first page; an empty string is a 422.
    return_fieldsNosurface defaultField projection
    user_idNoanonymousSigned-in shopper
    personalizeNotruePersonalize 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.

    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:

    QueryPrice constraint
    lámpa 30 000 Ft alattAt most 30,000
    lámpa 5000 Ft-tólAt least 5,000
    lámpa 5000 és 10 000 Ft közöttFrom 5,000 to 10,000, inclusive
    lámpa max. 20 ezer alattAt 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:

    LanguageQueryPrice constraint
    Finnishlamppu 100 euroon astiAt most 100
    Finnishlamppu 100 eurosta alkaenAt least 100
    Portuguesecandeeiro até 100 eurosAt most 100
    Portuguesecandeeiro entre 50 e 100 eurosFrom 50 to 100, inclusive
    Sloveniansvetilka do 100 evrovAt most 100
    Sloveniansvetilka od 50 do 100 evrovFrom 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:

    LanguageQueryPrice constraint
    Greekλάμπα κάτω από 100 ευρώAt most 100
    Greekλάμπα μεταξύ 50 και 100 ευρώFrom 50 to 100, inclusive
    Swedishlampa under 100 krAt most 100
    Swedishlampa från 50 till 100 kronorFrom 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.

    {
    "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"
    }
    FieldMeaning
    hitsThe ranked results, best first. Flat objects; @id and @type always present.
    totalExact count, or null when the result set is too large to count exactly
    total_approxAlways present; the fallback when total is null
    request_keyIdentifies this result set — pass it to /facets
    guidDeprecated alias of request_key, same value — see the overview
    next_page_cursorPass back as cursor for the next page, or null
    pagination_statusWhy there is or is not a cursor — see pagination

    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 AbortController per 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.

    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.

    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.

    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.