Render your own search results
VeryQuery's "Build my own" render mode keeps everything the app already does for you (catalog sync, ranking, signals) and hands the search results back as plain data, so your developer can render them with your own theme markup instead of our grid. This guide is Shopify-specific: it uses Shopify's App Proxy, and the request and response details below are particular to the Shopify app.
Which path is this?
VeryQuery on Shopify gives you three ways to handle search, set on the VeryQuery app embed in your theme editor:
- Show VeryQuery's results
- We render the results page for you - a ranked grid with filters, sort, and add-to-cart. No code. See Storefront search.
- Build my own with the VeryQuery API (this page)
- We rank and return the results; you render them with your own markup. You stay on your normal Shopify theme, the app keeps syncing your catalog and capturing signals, and you only write the rendering.
- Keep my current search, collect insights
- Your theme's search stays; VeryQuery just captures the demand data. No rendering at all.
This is also different from a fully headless build (Hydrogen, Remix, a custom storefront). In that case you own everything, including catalog sync, and call our /v1 API with an API key from your own server. Here, the installed app already owns ingest and signals, and you call the storefront proxy from the browser. Lighter, but Shopify-only.
What the app does for you vs. what you do
- The app handles
- Catalog sync (via Shopify webhooks), ranking, and signals: cart-adds are captured automatically, and every search call you make records the shopper's query for your demand map. Purchases come in through the app's order webhook. You don't call /v1, you don't host a server, and you don't register your own App Proxy.
- You handle
- Calling the search proxy and rendering the cards. That's it.
Turn it on
- In your Shopify admin, open Online Store, Themes, Customize.
- Open App embeds (the puzzle-piece icon) and make sure the VeryQuery embed is on.
- Set its Search setting to "Build my own with the VeryQuery API", and Save.
In this mode we don't render anything on your storefront (your theme's native search page stays in place until your own code replaces it), and the embed stops capturing queries on its own - your search calls below are what record them. So wire up the calls before relying on the demand data.
Call search
From your theme's JavaScript, POST to the proxy path on your own storefront domain. Shopify forwards it to VeryQuery and signs the request, so there's no API key in the browser and no CORS to configure - it's a same-origin request.
const res = await fetch('/apps/veryquery/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: shopperQuery, // the text the shopper typed
limit: 24, // page size
sessionId: sessionStorage.getItem('vq_session') || undefined,
}),
});
const data = await res.json();
// data.items is an array of result cards (shape below)
Request fields (all optional except one intent):
- query
- The shopper's text query. Use this for normal search.
- lens / item
- Alternatives to query, mutually exclusive with it. lens is a category-lens id (for a "browse this category" view); item is a product id (for "more like this"). Both are navigation, not recorded searches.
- axis
- relevance (default), price-asc, or price-desc.
- filters
- { minPrice, maxPrice, inStockOnly }.
- limit / offset
- Page size and offset. For "load more," request the next page with offset increased by limit while hasMore is true.
- sessionId
- The embed's vq_session id (read it from sessionStorage). Passing it links this shopper's query to their cart-adds and purchases in your demand map. See Sessions below.
The response
The top level:
{
"ok": true,
"query": "merino sweater",
"count": 18, // relevant matches found
"hasMore": true, // more pages exist beyond this one
"searchId": "sq_...",
"items": [ /* cards, see below */ ]
}
Each card:
{
"id": "7422585307191", // Shopify product id
"title": "Brushed Fleece Hoodie",
"handle": "brushed-fleece-hoodie",
"brand": "Maison Vega",
"price": 42.0, // major units (see Money note)
"compareAtPrice": 52.0, // reference price; greater than price = on sale
"available": true, // any variant purchasable
"images": [
{ "url": "https://cdn.shopify.com/...", "variantIds": ["111"] }
],
"options": ["Color", "Size"], // dimension names (present if the item has variants)
"variants": [
{ "id": "111", "values": ["Olive", "M"], "available": true, "price": 42.0 },
{ "id": "222", "values": ["Olive", "L"], "available": false, "price": 42.0 }
],
"matched": { "variantId": "111", "values": ["Olive", null] }
}
- options / variants
- options is the list of dimension names. Each variant's values array is positional against it: values[0] is the Color, values[1] is the Size, and so on. To build a Color dropdown, collect the distinct values[0] across variants. Both are present only when the product has variants.
- matched
- When the query referred to a variant value (a color, a size), this is the variant we matched, in the same positional form (null for dimensions the query didn't pin). Use it to pre-select the swatch and to deep-link the card to that exact SKU.
- images
- Each image's variantIds tells you which variants it belongs to; an image with an empty variantIds is the hero. Swap the displayed image to match the selected variant.
Rendering notes (Shopify)
- Product link: /products/{handle}. To land on a specific SKU, append ?variant={variantId} (the matched variant, or the shopper's current dropdown selection). Shopify honors the ?variant= form; the ?Color=… form is ignored by Dawn-derived themes.
- Sold out: use the card's available, or a specific variant's available, to render sold-out treatment and disable add-to-cart.
- Add to cart: POST the chosen variantId to Shopify's /cart/add.js as usual.
Sessions and the demand map
The VeryQuery embed mints an anonymous vq_session id (per browser tab) and stores it in sessionStorage. It also captures cart-adds and stamps the session through checkout automatically. To complete the picture, read that id and pass it as sessionId on every search call:
const sessionId = sessionStorage.getItem('vq_session') || undefined;
That's what joins a shopper's query to what they later add to cart and buy. It's optional: without it, queries still feed the demand map, they just aren't linked to that shopper's downstream activity. Never send a customer identifier here; the session id is anonymous and resets when the tab closes.
Other surfaces, same shape
The other VeryQuery surfaces are also reachable through the proxy and return the same card shape (under a results key), so the same rendering code reuses:
- Similar items
- POST /apps/veryquery/similar-items/{productId} - "more like this" for a product page.
- Smart Categories
- POST /apps/veryquery/category-lenses - auto-derived category tiles (a different tile shape; see the reference).
- Trending / Curated
- POST /apps/veryquery/trending-items and /apps/veryquery/curated-items.
You can also just place our prebuilt blocks for these from the theme editor and only hand-render search; they're independent.
Empty results and errors
On a successful call with no matches, items is an empty array. The call also returns ok: true with empty items and a reason when VeryQuery isn't serving:
- NO_ACTIVE_PLAN / USAGE_LIMIT_REACHED
- The property has no active plan, or has hit its plan's usage limit. Fall back to native search.
- SHOP_NOT_CONNECTED
- The store isn't connected (or the app was removed). Fall back to native.
- 401 PROXY_SIGNATURE_INVALID
- The request didn't arrive through Shopify's App Proxy. This happens if you call the backend directly instead of the storefront /apps/veryquery/* path; always call the relative path from storefront code.
Treat any non-result response as "show the shopper your theme's normal search" so the experience degrades gracefully.
Full reference
The exact request and response schemas for every storefront-proxy endpoint, including the card shape, live in the API reference under "Storefront proxy (Shopify)".