> ## Documentation Index
> Fetch the complete documentation index at: https://docs.depict.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom search UI: quickstart

> Build your own search interface on the Depict storefront search API — the same endpoints the Depict search modal calls.

<Warning>
  **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.
</Warning>

This guide is for developers building a custom search interface against the
Depict storefront search API. It covers the four calls a working search UI
needs, plus the tracking events that keep analytics and attribution intact.

The API is public and unauthenticated for the read endpoints: it is called
directly from the shopper's browser, and every request identifies the store
with a merchant id. There is no API key to manage for search.

## What you need

| Value         | Example             | Where it comes from                                                                                                                                 |
| ------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `merchant_id` | `search-1234567890` | Depict onboarding. On a Shopify store running the Depict app it is `search-` followed by your Shopify shop ID.                                      |
| `locale`      | `en`                | The storefront language. Drives translated product attributes and the localized UI strings the API returns.                                         |
| `country`     | `US`                | ISO 3166-1 alpha-2 country of the shopper. Resolved server side to one of your Shopify markets, which determines prices, currency and availability. |

`locale` and `country` are required on every request. A country that is not
mapped to one of your markets returns `404`, as does an unknown `merchant_id`.

## Base URL

```
https://runway.depict.ai
```

## 1. Run a search

`POST /search_v2` is a plain HTTPS POST — one request per page, no WebSocket
or Server-Sent Events — whose **response body is streamed** as
newline-delimited `key:value` lines, so you can render products before the
response has finished. Each line is a key, a colon, then JSON. If you would
rather not stream, await the whole body and split it on newlines; both
patterns are in
[REST or streaming?](/shopify-search/custom-search-ui/api-reference#rest-or-streaming).

<CodeGroup>
  ```bash curl theme={null}
  curl -N https://runway.depict.ai/search_v2 \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/x-ndjson' \
    -d '{
      "query": "loafers",
      "merchant_id": "YOUR_MERCHANT_ID",
      "locale": "en",
      "country": "US",
      "page": 1,
      "session_id": "SESSION_ID",
      "search_id": "SEARCH_ID"
    }'
  ```

  ```javascript fetch theme={null}
  async function search(body, onEvent) {
    const response = await fetch("https://runway.depict.ai/search_v2", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/x-ndjson",
      },
      body: JSON.stringify(body),
    });
    if (!response.ok || !response.body) {
      throw new Error(`Search failed: ${response.status}`);
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    // Some keys arrive as several fragments; accumulate per key and parse when complete.
    const accumulated = {};

    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });

      let newline;
      while ((newline = buffer.indexOf("\n")) !== -1) {
        const line = buffer.slice(0, newline);
        buffer = buffer.slice(newline + 1);

        const colon = line.indexOf(":");
        if (colon === -1) continue;
        const key = line.slice(0, colon);
        accumulated[key] = (accumulated[key] ?? "") + line.slice(colon + 1);

        try {
          onEvent(key, JSON.parse(accumulated[key]));
        } catch {
          // Not a complete JSON value yet — wait for the next fragment.
        }
      }
    }
  }

  await search(
    {
      query: "loafers",
      merchant_id: "YOUR_MERCHANT_ID",
      locale: "en",
      country: "US",
      page: 1,
      session_id: sessionId,
      search_id: searchId,
    },
    (key, value) => {
      if (key === "keyword") renderProducts(value);
      if (key === "keyword_page_info") setPagination(value);
      if (key === "filter_facets") setAvailableFilters(value);
      if (key === "error") showError(value.error_hint);
    }
  );
  ```
</CodeGroup>

A default response contains four lines, always in this order:

```
filter_facets:["colors","material","product_type"]
sorts:[{"field":"_relevance","order":"desc","meta":{"values":["desc"]}},{"field":"price","order":"asc","meta":{"values":["asc","desc"]}}]
keyword:[{"id":"9606764101971","title":"Karlberg – Black Calf","price":399.0, ...}]
keyword_page_info:{"page":1,"has_next_page":true,"has_prev_page":false,"is_fallback":false,"n_hits":33}
```

* `keyword` is the product array — the field is not called `products`.
* `filter_facets` lists the **filter field names** enabled for your account, not
  values or counts.
* `sorts` lists the sort options your account offers, on every response. Read
  the `sort` request field's allowed values from here rather than hardcoding
  them.
* `keyword_page_info` carries pagination: `has_next_page` says whether to
  request the next page, `n_hits` is the total. Page size is the request's
  `limit` (default 30, up to 250). See
  [Pagination](/shopify-search/custom-search-ui/api-reference#pagination).
* `error` can appear mid-stream if retrieval fails after headers were sent.
  The search failed — render an error state. See
  [Retries](#behaviour-worth-knowing-before-you-build) for why this case is
  not the same as a failed request.

See the [API reference](/shopify-search/custom-search-ui/api-reference)
for every field, every stream key, filters and pagination.

<Note>
  `search_id` identifies one search. Generate a new one when the shopper
  submits a query, and reuse it for the pages of that search and for every
  tracking event about its results. **Changing a filter is a new search** —
  the modal mints a fresh `search_id` for it, because the shopper is now
  looking at a different result set. Reusing the old id there would merge two
  result sets into one row in your analytics. `session_id` is one id per
  browser tab, stable across searches.
</Note>

## 2. Query suggestions

`GET /autocomplete_v2` returns up to three suggestions for a partial query.
Call it as the shopper types.

<CodeGroup>
  ```bash curl theme={null}
  curl -G https://runway.depict.ai/autocomplete_v2 \
    --data-urlencode 'query=loaf' \
    --data-urlencode 'merchant_id=YOUR_MERCHANT_ID' \
    --data-urlencode 'locale=en' \
    --data-urlencode 'country=US'
  ```

  ```javascript fetch theme={null}
  const params = new URLSearchParams({
    query: "loaf",
    merchant_id: "YOUR_MERCHANT_ID",
    locale: "en",
    country: "US",
  });
  const response = await fetch(
    `https://runway.depict.ai/autocomplete_v2?${params}`
  );
  const { suggestions } = await response.json();
  ```
</CodeGroup>

```json theme={null}
{ "suggestions": ["loaf**ers**", "loaf**ers in dark brown suede**"] }
```

Suggestions are markdown: `**bold**` marks the part the shopper has not typed
yet. Render the markdown, but **search the plain text** — strip the markers
before passing the suggestion to `/search_v2`, or the query will not match.

Responses are CDN-cached, so late responses for an earlier keystroke can arrive
after newer ones. Track which query each response belongs to and ignore stale
ones.

## 3. Empty state

Before the shopper types anything, `GET /empty-state-products` gives you
products to show, starter suggestions, and the localized UI strings for your
`locale`.

```bash theme={null}
curl -G https://runway.depict.ai/empty-state-products \
  --data-urlencode 'merchant_id=YOUR_MERCHANT_ID' \
  --data-urlencode 'locale=en' \
  --data-urlencode 'country=US'
```

```json theme={null}
{
  "products": [ /* same product shape as search */ ],
  "total_count": 6,
  "suggestions": ["What shoes should I wear to a wedding?", "..."],
  "localized_strings": { "view_all": "View all", "no_results": "...", "...": "..." }
}
```

`localized_strings` holds 46 translated interface strings (button labels, facet
labels, sort labels, empty and error messages). Using them keeps a custom UI
translated in every locale you already support, and picks up merchant-specific
overrides configured in Depict.

## 4. Send tracking events

Analytics in the Depict portal, and the ranking that feeds on it, come from
client-side events. A custom UI must send them itself — nothing is inferred
server side from a search request.

```bash theme={null}
curl https://runway.depict.ai/events \
  -H 'Content-Type: text/plain' \
  -d '[{
    "event_type": "product_click",
    "merchant_id": "YOUR_MERCHANT_ID",
    "session_id": "SESSION_ID",
    "device_id": "DEVICE_ID",
    "locale": "en",
    "country": "US",
    "search_id": "SEARCH_ID",
    "product_id": "9606764101971",
    "query": "loafers",
    "position": 0
  }]'
```

At minimum, send `product_impression` and `product_click` for search results,
and `add_to_cart` and `purchase` with the `search_id` of the search that led to
them. Full event list, required fields and the attribution rules are in
[Tracking events](/shopify-search/custom-search-ui/tracking).

## Behaviour worth knowing before you build

* **Pagination is page-based.** Send the same body with `page: 2` for the
  next `limit` products (default 30, up to 250). `has_next_page` is true
  exactly when a page came back full, so a page past the end is empty rather
  than an error. Keep `search_id` across the pages of one search; a query,
  filter or sort change is a new search with a new id and `page: 1`. Rules and
  a worked three-request example are under
  [Pagination](/shopify-search/custom-search-ui/api-reference#pagination).
* **Filters re-run the search.** There is no separate filter endpoint: add a
  `filters` array to the same `/search_v2` request. Debounce rapid filter
  toggles, and cancel the in-flight request when a new one starts.
* **Sorting is a request field.** Add `sort: { "field": "created", "order":
  "desc" }` to the same `/search_v2` request to reorder results server-side,
  with correct ordering across pages. Read the offered options from the
  response's `sorts:` line rather than hardcoding them — an unoffered
  field/order combination returns `422` naming what is offered, never a
  silently different ordering. Omit the field for relevance order.
* **`is_fallback: true`** means no good match was found for the query as typed
  and a relaxed search answered instead. The Depict modal labels these results
  as close matches rather than presenting them as exact hits.
* **Retries.** The modal retries a failed *request* twice — three attempts in
  total — with a randomised 0.5–2s wait between them. That covers non-`200`
  responses and dropped connections. A mid-stream `error` line is **not**
  retried: the modal stops and shows an error, because the server already
  decided the search failed. Decide deliberately which of the two your UI
  does.
* **Response size.** Cap how much you read from a stream. The Depict modal
  aborts a `/search_v2` response after 100 kB.

## Next

<CardGroup cols={2}>
  <Card title="API reference" icon="code" href="/shopify-search/custom-search-ui/api-reference">
    Every endpoint, field, stream key and filter.
  </Card>

  <Card title="Filters and suggestions" icon="sliders" href="/shopify-search/custom-search-ui/filters-and-suggestions">
    Building filter controls and rendering suggestions, and the traps in both.
  </Card>

  <Card title="Tracking events" icon="chart-line" href="/shopify-search/custom-search-ui/tracking">
    The events to send, and how attribution works.
  </Card>
</CardGroup>
