Storefront Search SDK
Call the storefront search pipeline from your own JavaScript — typed search, instant suggestions, voice, photo and similar-product queries, with results you render yourself.
Storefront Search has two ways in.
The widget is the no-code path: you write HTML, mark it up with data-octocom-* attributes, and our script binds it to the search pipeline and renders into it. Most stores want this — it's covered on the Storefront Search page.
The SDK is this page: the same search client, exposed as a JavaScript object you call yourself. Use it when you're rendering results in your own code — a React storefront, a custom results page, a headless front end — and want the pipeline without our DOM rendering.
Both talk to the same endpoints and both are already on the page. There is no package to install.
Getting the client
The SDK ships inside the Octocom embed script you already have on your store. Once it loads on a business with Storefront Search configured, it publishes the client at window.octocomSearch.
The script loads asynchronously, so your code may run first. Handle both cases:
function withSearch(fn) {
if (window.octocomSearch) return fn(window.octocomSearch);
window.addEventListener("octocom:search-ready", (e) => fn(e.detail), {
once: true,
});
}
withSearch(async (search) => {
const { results } = await search.search("black office chair");
console.log(results);
});In React, the same thing as a hook:
function useOctocomSearch() {
const [client, setClient] = useState(() => window.octocomSearch ?? null);
useEffect(() => {
if (client) return;
const onReady = (e) => setClient(e.detail);
window.addEventListener("octocom:search-ready", onReady, { once: true });
return () => window.removeEventListener("octocom:search-ready", onReady);
}, [client]);
return client;
}Methods
Every method returns a promise and takes an optional signal (an AbortSignal) so you can cancel on unmount.
| Method | What it does | Cost |
|---|---|---|
suggest(query, opts) | The search-as-you-type panel. Query completions, a few top products, category/brand chips, a count. | Cheap — safe per keystroke. |
search(query, opts) | Full pipeline for a submitted query. Ranked results only. | A full search. |
searchPanel(query, opts) | Full pipeline, answered in suggest's shape — results and chips, count, refinements. | Same as search. |
voiceSearch(blob, opts) | Transcribe a recorded utterance and search for it, in one round trip. | A search + transcription. |
transcribe(blob, opts) | Transcript only, for dictate-and-review. | Transcription. |
imageSearch(blob, opts) | Search by photo. | A search + a vision call. |
similarProducts(productId, opts) | "You may also like" for a product page. | Cheapest — no AI call. |
getUiConfig(opts) | The business's dashboard-configured widget HTML. Only needed if you're doing your own binding. | One indexed read. |
Search-as-you-type
suggest() is built to run on every (debounced) keystroke. It's a keyword probe, not a full search — no AI call — so it's cheap and fast, and its job is to produce a dropdown of query strings, not results.
input.addEventListener("input", async () => {
const panel = await search.suggestLatest(input.value);
if (!panel) return; // superseded by a newer keystroke
render(panel); // { suggestions, products, categories, brands, totalCount }
});suggestions are full query strings ready to submit. products is a preview handful from the same probe — enough for a dropdown column, not a results page. categories and brands are navigable chips. totalCount is how many products match the typed words at all.
Submitted searches
When the shopper hits Enter, you have two choices.
search() gives you the ranked results and nothing else:
const { results, relaxed } = await search.search(query, { topK: 24 });searchPanel() runs the same pipeline but answers in the shape suggest() uses — the results plus the chips, the count and refinements, all describing the results you're about to show:
const panel = await search.searchPanel(query, { topK: 24 });
// { products, suggestions, categories, brands, totalCount, relaxed }Use searchPanel() when your results page has a facet rail, a "N results" headline, or "narrow your search" chips. Getting them this way is one request; the alternative — calling search() and suggest() together — is a wasted search's worth of latency, and its chips would describe the probe's result set rather than the ranked one you're displaying.
Two things to know about it:
- It is a full search. Same cost, same rate limit, same latency as
search(). It does not belong on a keystroke; that's whatsuggest()is for. totalCountis notproducts.length. The ranked list is capped bytopK;totalCountanswers "how many products match at all", which is what a results headline wants.
relaxed: true on either means exact retrieval came back thin and the results were recovered by relaxing the query — label them "similar products" rather than presenting them as exact matches.
Avoiding stale results
Every ranking method has a *Latest twin — searchLatest, suggestLatest, searchPanelLatest — that aborts whatever request was in flight and resolves null for a response that has since been superseded:
const response = await search.searchPanelLatest(query);
if (response === null) return; // a newer query is already runningThey share one slot, so a keystroke's suggestLatest and an Enter's searchPanelLatest supersede each other — the right behavior for a single search box. Render every non-null result unconditionally and out-of-order responses can't flash over fresh ones.
Errors
Failures throw OctocomSearchError with a kind:
kind | Meaning |
|---|---|
rateLimited | HTTP 429. Back off — the caps are per-IP and per-business. |
http | Any other non-2xx. 401 usually means the origin allowlist. |
network | The request never completed. |
Aborts (yours, or a superseded *Latest call) surface as a DOMException named AbortError, not as an OctocomSearchError — so "the shopper typed another character" stays distinguishable from "search is down".
On a live storefront, prefer leaving the previous results on screen to clearing them on error.
Panel data on your results page
If you're using the widget rather than the SDK, you can still have the chips and count beside your results — add their markers to your Results template (or your Overlay template) and the widget switches to the panel route automatically. No markers, no extra request: templates that don't ask for the data don't pay for it.
| Marker | Behavior |
|---|---|
data-octocom-panel-categories-container + -category-template | Category chips for the current results. data-octocom-field="text" / "count" / "url". |
data-octocom-panel-brands-container + -brand-template | Brand chips, same fields. |
data-octocom-panel-total-count | Single element — total keyword matches for the query, hidden while unknown. Not the number of cards shown. |
data-octocom-refinements-container + -refinement-template | "Narrow your search" chips. data-octocom-field="text" / "role". Clicking one runs the refined query. |
data-octocom-panel-categories-section / -brands-section / -refinements-section | Optional wrapper around a section's heading + container, hidden as a unit when that section is empty. |
The refinement markers are deliberately named differently from the dropdown's data-octocom-suggestion-* pair, so a results template can carry both without the widget mistaking one for the other.
Chips are only ever shown for values your catalog feed gives a navigable URL for. If categories comes back empty, your feed carries category names but no category links — the same vocabulary still reaches shoppers through refinements, which need no URL.