403).
Workflow Examples has the client setup. The shopper is hunting for a
Samsung Galaxy Watch 6 (0186665f-f0a1-4c79-976a-919b5b1c2daa, variant e98631a9-…, $299), which
carries forward through every surface.
1. Searching the catalog
Two search surfaces answer different questions. Both return product ids and relevance scores rather than full product objects, so the cards are hydrated from the ids (step 5).1a. Text search (keyword) — a match
searchProducts matches the literal query against product names and attributes. It is exact and
fast, which suits a search box where the shopper types a brand or model. Publishable key.
Response
1b. Text search — an empty result
A mistyped query, or one for something the store doesn’t stock, still succeeds with200: the same
envelope comes back with an empty results array and totalResults: 0. There is no error to catch,
so an empty state branches on results.length.
Response
1c. Semantic search (natural language)
semanticSearch matches the meaning of a conversational query against product meaning using AI
embeddings, rather than literal keyword overlap, which fits a shopper who describes what they want
(“wireless earbuds for the gym”) instead of naming it. Each hit carries a similarity score;
minScore sets the floor (default 0.3) and limit caps the count.
Response
totalResults is the size of the candidate set considered, not the number returned — here 50
products were ranked and the top 8 above minScore came back. A higher minScore tightens
relevance, a lower one widens the net. Semantic hits carry a score but no matchReason, which is
a text-search field.1d. In-catalog search with a category filter and paging
The two surfaces above search the index. A results grid usually reads the catalog directly instead:listProducts takes a search term, a categoryId filter, and cursor paging in one call, and
returns full product rows rather than ids. This is the shape behind a “Headphones → Sony” filtered
grid.
variant_id, sku, price, stock) plus
category_name/subcategory_name, so the grid needs no second call. Paging passes
pagination.next_cursor back as cursor:
Response
1e. Browsing by brand — a featured-brands row into a brand page
Brand is a second axis alongside categories.listBrands returns the catalog’s distinct brands with
a product count, most products first, which is the shape a featured-brands strip renders from; a
click passes the brand to listProducts({ brand }).
2. Recommendation rails — every type, and the fallback
Recommendations back the “you might also like” and “frequently bought together” rails. This is the one discovery call that is secret-key only, running on your server (a publishable key returns403).
One method, getRecommendations, serves every rail through the type field, and every response
shares the same { type, recommendations[], fromCache, computedAt } envelope, each item carrying
productId, score, and reason.
A rail is never empty, because the engine cascades through fallbacks: a cold product with no
co-purchase or similarity signal falls back bundle → also-bought → similar → trending → random. An
also-bought rail on a product with no purchase history therefore comes back with reason strings
reading “Similar product…” or “Trending #1…” — the requested type had nothing behind it and the
engine served the next-best signal it had, under the same envelope.
Each rail below is anchored on the Watch the shopper is viewing (0186665f-…).
2a. Similar products
Response
2b. Frequently bought together (also-bought)
Ranks products co-purchased with the anchor. Without co-purchase history the engine falls back to the
similarity signal, so the reason strings are the similar rail’s and fromCache is false — this
rail was computed fresh.
Response
2c. Bundle pairings (bundle) — complements, not substitutes
bundle surfaces complementary products that pair with the anchor — a case for the watch — rather
than alternatives to it. The reason carries the co-purchase rate, here 0%, so these are ranked by
complementarity alone, and the scores near 0.32 are what a pairing suggestion scores rather than a
close substitute.
Response
2d. Trending and New — store-wide rails (no anchor)
trending and new take no productId, which makes them homepage rails. trending ranks by recent
sales velocity, reported as units/day in the reason; new ranks by recency.
Response
Response
2e. Personalized (personalized) — for a signed-in shopper
Identify the shopper with a credential header (x-auth-token, x-external-auth, or x-idp-token);
the recommendations are keyed to whoever that credential resolves to, not to a customer id in the body.
Without a valid credential (or per-shopper signal) it falls back to trending, which is why the
reason strings here are the trending ones. The same call returns personalized ranks once the
customer’s history builds.
Response
Asking for a rail on a product that doesn’t exist returns
404. A publishable key on any
recommendation call returns 403 — keep these on your server.2f. Discovery shelves — “Popular now” and a product-page “hot” badge (publishable key)
Where recommendations are personalized and secret-key, Discovery is the non-personalized counterpart that renders straight from the browser: windowed rankings built from live shopper signals — views, cart-adds, view-to-purchase. It is publishable-key and available on every plan. Each call returns{ metric, window_hours, products: [{ product_id, score }] }, with cards hydrated through
products.getProduct. score is a raw sortable signal — a count, or a ratio for converting —
already ordered highest-first.
Response
storefront here uses a publishable key, since Discovery is browser-safe. Allowed windowHours
differ per metric — 1|24|168|720 for most-viewed, 24|168 for carts, 168|720 for converting —
and an unlisted value returns 400. Discovery covers organic “what’s hot” shelves; Recommendations
covers personalized and anchored rails.3. Capturing browsing behaviour for the next rail
The next-item rail sharpens on what the shopper does as they browse. recordEvent logs one event
per call against a variant_id (or product_id), grouped by the same session_id the anonymous
cart uses. It is publishable-key and safe in the browser, and it is fire-and-forget: it returns 202
immediately without blocking the page. Three event_type values exist, one per shopper action.
3a. view — the shopper opens the Watch’s page
Response
3b. add_to_cart — they add it
3c. add_to_wishlist — or they save it for later
session_id, the
next rail (secret key) answers what the shopper is likely to look at next, from what other shoppers
viewed after the same sequence:
Response
4. Dynamic, location-aware prices
Search and recommendations return product ids; prices come next, in the shopper’s currency.getProductPrices resolves each product’s display price from the shopper’s location — coordinates
(latitude/longitude) or a placeName — matching it to a region and converting from the store’s
base currency. USD is the store default here, with GBP and EUR enabled. The same Watch priced from
three locations covers every conversion shape.
4a. New York → USD (store base, rate 1)
No conversion happens:display_currency is USD, exchange_rate is 1, and
price_breakdown.currency_conversion is null.
Response
4b. London → GBP (rate 0.79)
WithplaceName: 'London, UK' (or London coordinates) the same product comes back priced in GBP, and
currency_conversion is populated with the from/to/rate/converted_amount.
Response
4c. Berlin → EUR (rate 0.92)
latitude: 52.52, longitude: 13.405 resolves to EUR — the same envelope, different rate and
converted_amount.
Response
4d. No location → store default (USD)
Without coordinates or aplaceName, pricing falls back to the store’s default currency, USD here —
the same shape as New York, with no conversion.
5. Browsing by category
A navigation menu or category landing page starts fromlistCategories (publishable key), then
filters the catalog or pricing by categoryId — the same filter the grid used in step 1d.
Response
listProducts and getProductPrices accept a categoryId to scope a catalog read to that
category. The Watch lives in Wearables (3310356e-…), the same product this page followed through
search, recommendations, behaviour, and pricing:
Categories are a fixed, store-wide set. Merchants nest subcategories under them to any depth for
deeper navigation;
storefront.taxonomy.listSubcategories({ tree: true }) fetches them as a tree,
and { rootOnly: true } / { parentId } a single level.What’s next
- A shopper who has found a product continues into the storefront checkout — cart, sign-in, and payment.
- Reviews & Messaging opens with the wishlist flow, moving a saved Watch into the cart before the post-purchase surfaces.
- Returning customer covers order history, returns, and store credit for a shopper coming back.
- Search, recommendations, and pricing all accept a marketplace operator key — see Marketplace.

