Skip to main content
This API will be removed at the end of this year. It is the current-generation storefront search API, and it is the supported and recommended way to build a custom search UI today — the Depict search modal runs on exactly these endpoints. A successor API is coming, and every team building on this one should plan for a migration before year end. Migration documentation will follow; ask your Depict contact to be notified when it is published.
The API gives you filter field names and a product array; the filter controls themselves are yours to build. So is the markup for query suggestions. Both are straightforward to get working and easy to get subtly wrong — the failures on this page all pass a quick manual test and only show up once a shopper combines two things. Everything here is client-side behaviour. See the API reference for the wire shapes.

Filter UIs

Where option values come from

filter_facets lists the field names enabled for your account. It carries no values and no counts:
Values come from one of two places:
  • size is server-counted. Sizes live on variants and results are grouped by product, so they cannot be read off the product cards. When the facet is enabled the backend sends them on their own stream key, pre-sorted, and scoped so that selecting one size does not remove the others:
  • Every other field is derived by you, from the products in the response.
That second case is where the trap lives, because the products in the response are already scoped by the filters you sent. Derive naively and each facet narrows itself.

A facet must never narrow its own option list

Say gender offers female and male. The shopper picks male, you send the filter, and the response now contains only men’s products — so the only gender value you can derive is male. Rebuild the control from that and female disappears. The multi-select has become a one-way door: the shopper cannot get back, cannot select both, and the group looks broken. Cross-facet narrowing is the opposite case and it is correct. If the shopper filters by gender, the material options should shrink to the materials men’s products actually come in. Only a facet’s own filter must be excluded from its own options. The fix is to remember what each field offered before it was filtered, and re-offer those values at count 0 while that field has a selection:
Call this with every keyword payload you receive, including later pages — more products means more values observed, and the set only grows within one query. Key the memory on everything that changes the vocabulary — the query, and also locale and country. Attribute values are localised, so the same field returns different words per market; a market switch mid-session must start the memory over rather than merge two languages into one control. The memory only ever re-offers values your catalogue actually returned in this locale, which is why it is a memory and not a hardcoded list. Then build the options for one field:
This approximates a disjunctive facet rather than implementing one. A true disjunctive count for gender would be computed over “every filter except gender”, which the client cannot see — it only ever receives products matching all filters. So a re-offered value can occasionally be an option that yields nothing once a second field is also filtered. That is the right trade: an option that returns no results is recoverable, an option that has vanished is not.

Keep a selected value visible, even at count 0

A selected value must stay in the list whatever the counts say. It is not just an option — it is the control that clears the filter. Drop it and the shopper is holding an active filter with nothing on screen to switch it off, and the only way out is a global “clear all”. This is why the snippet above seeds selected separately from knownValues: the memory is only consulted while the field has a selection, but the selection is always re-added.
The dead end is easy to miss in testing because it needs two filters to reproduce: a combination that returns no products at all will empty every derived option list at once. Test a deliberately empty result set and confirm each active filter is still visible and still clearable.

Zero-count options must stay clickable

Greying out or disabling a zero-count option looks tidy and breaks the control. Consider a single-select-style gender facet with male selected. In the men-filtered results female has count 0. Disable it, and the shopper can never switch to female — the only enabled option is the one already selected. The facet is stuck on its first choice for the rest of the session. Render zero-count options as normal, clickable options. Clicking one should do what clicking any option does: add it to the selection, or swap the selection if your control is single-select. If you want to signal that an option is thinly populated, do it with styling that does not remove the click target.

Optional refinements

  • Hide an empty group. A field whose derived options are empty and which has no selection can be hidden — there is nothing to choose. Do not hide a group that has a selection, for the reason above.
  • Seed closed vocabularies. Some fields have a small, fixed set of values — gender is usually female / male, sometimes with unisex. If you know the full set for your catalogue, you can seed the control statically and give each value a display label (“Women”, “Men”) instead of showing the raw indexed value. Two cautions: the values are localised, so a static list is only safe for the locales you have actually checked, and the value you send in a filter must still be the exact indexed value, not the label.
  • Do not re-sort server-counted options. They arrive in display order.

Sending the filter

Each entry is { field, operator, value }, and the values are echoed back verbatim from what you derived:
Multi-select facets use in with an array. Full operator and field list, and the rules about which fields are safe to filter on, are in the API reference.
Changing a filter is a new search: re-run /search_v2 with a fresh search_id, debounce rapid toggles, and cancel the in-flight request when a new one starts.

Rendering query suggestions

The format

/autocomplete_v2 returns suggestions in a markdown-lite form where ** marks the completion — the part the shopper has not typed yet:
The typed prefix stays plain; the span inside ** is what you bold. Split on **, escape every part, and wrap the odd-indexed parts:
Search and match against the plain text, never the raw string. The markers are presentation only — passing loaf**ers** to /search_v2 as the query will not match what the suggestion was validated against.

Whitespace: the disappearing space

When the completion starts a new word, its leading space is inside the ** span. After parsing you have two sibling nodes, and the space is the first character of the second one:
If the row containing them is a flex container, that leading space is dropped and the suggestion renders as loafersin dark brown suede. The parsing is correct; CSS ate the space. Either set white-space: pre-wrap on the element that holds the suggestion text, or do not make that element a flex container — put the flex layout on a wrapper (icon, text, chevron) and let the text itself be a plain inline container.
The same applies to any layout that reflows text nodes independently, not just flex. If the space is present in your parsed output but missing on screen, it is a CSS problem, not a parsing one.

Ordering

Suggestion responses are CDN-cached, so a response for an earlier keystroke can arrive after a newer one. Record which query each response belongs to and discard stale ones, or the list will flicker back to older completions as the shopper types.

Next

API reference

Every endpoint, field, stream key and filter.

Tracking events

The events to send, and how attribution works.