Do these 3 steps to get a working search widget.
1) Add this script
Paste into your theme/site (preferably in the head).
CDN script (pinned)
Pin the version. For SRI/CSP hardening, see Advanced / Security.
<script src="https://cdn.inops.io/inops-web-sdk@1.2.0/index.global.js"></script>2) Add the widget div
Place it where the search UI should appear.
Search widget div
<div data-widget="search" data-id="my-search-widget"></div>3) Set your Search Key
Copy it from the portal (Shop → Security → SearchKey).
Set data-search-key
<div data-widget="search" data-search-key="YOUR_SEARCH_KEY" data-id="my-search-widget"></div>SDK installation (Quick Start · 5 minutes)
Three steps to get a working search widget on a Shopify theme or any site.Test: search, similar_products, campaignId.
Playground settings
Settings that apply to every flow request in this demo.Language
Applies to all requests below.
Feature 1 — Search
Runs userInput.type="search". Requires a 3+ character query (single-word for direct search, multi-word for intent-based).Use this for your main “search box” experience. The backend will return a streaming summary + ranked products.
Query
Tip: type 3+ characters to search. Single words trigger direct search, multi-word queries use intent-based matching.
JSON payload
Request body for POST /shop/flow/execute (headers omitted).
{
"language": "en",
"userInput": {
"type": "search",
"value": ""
}
}Result JSON
What you can expect back via SSE (summary + products + meta).
{
"summary": "",
"products": [],
"meta": null
}Feature 2 — Similar products
Runs userInput.type="similar_products". Requires productId.Use this on product pages to show “alternatives” or “you may also like” results. It takes a single seed
productId.To enable this section, first run Feature 1 so we can reuse a productId from the search results.
Reuse a productId (from your last search)
Preset via
VITE_DOCS_DEMO_PRODUCT_ID or paste your own below.productId (manual)
JSON payload
Request body for POST /shop/flow/execute (headers omitted).
{
"language": "en",
"userInput": {
"type": "similar_products",
"productId": "YOUR_PRODUCT_ID"
}
}Result JSON
What you can expect back via SSE (summary + products + meta).
{
"summary": "",
"products": [],
"meta": null
}Feature 3 — campaignId landing
Runs userInput.type="campaignId". Requires a merchant-configured campaignId.Use this to turn marketing traffic into an immediate search experience. Your storefront reads a
campaignId from the URL, and the backend resolves it to the configured searchTerm.campaignId (preset examples)
Configure presets via
VITE_DOCS_DEMO_CAMPAIGN_IDS (comma-separated). If the campaign is missing/expired, results are empty.campaignId (manual)
JSON payload
Request body for POST /shop/flow/execute (headers omitted).
{
"language": "en",
"userInput": {
"type": "campaignId",
"campaignId": "longboard_dummy_shop_campaign_01"
}
}Result JSON
What you can expect back via SSE (summary + products + meta).
{
"summary": "",
"products": [],
"meta": null
}Paste a Search Key from the portal (Shop → Security → SearchKey). Keys are read‑only and safe to embed in storefront HTML.
Demo shop data
The demo shop uses a surf catalog spanning 9 categories: Surfboards, Wetsuits, Accessories, Lifestyle, Fins, Leashes, Bags, Apparel, and Beginner Gear. The catalog supports bundle search — try multi-product queries to see assembled bundles across categories. All products are listed on the live dummy shop. You can use the same catalog as a dummy Shopify store: import the CSV into Shopify (Products → Import) to get a matching product set for testing.
Download dummy-shop-in-shopify-format.csv — Shopify-ready import file for the demo catalog.
In the playground above, try queries like longboard, wetsuit, leash, beginner soft-top, my boy wants to start surfing, or complete surf kit budget $500.
Live demo (demo shop)
Run the three core flows against our demo shop using your Search Key.When a shopper describes a multi-product need (e.g. "my kid wants to start surfing"), Inops interprets the intent, infers a budget if given, and assembles a curated product bundle across categories — one or more variant bundles may be returned.
How to detect bundles
Bundle results arrive as a separate SSE event: bundle-result. Listen for it alongside the standard products and summary-result events. Multiple bundle-result events may arrive in sequence — one per bundle variant (e.g. a budget option and a premium option).
Bundle response structure
bundle-result payload
// SSE event: type = "bundle-result"
{
"type": "bundle-result",
"data": {
"intent": "beginner surfer starter kit",
"budget": 500,
"groups": [
{
"category": "Surfboards",
"product": {
"id": "prod_abc123",
"title": "7ft Soft-Top Beginner Surfboard",
"price": 249,
"variantId": "variant_xyz"
}
},
{
"category": "Wetsuits",
"product": {
"id": "prod_def456",
"title": "3/2mm Full Wetsuit",
"price": 189,
"variantId": "variant_uvw"
}
},
{
"category": "Leashes",
"product": {
"id": "prod_ghi789",
"title": "7ft Surf Leash",
"price": 35,
"variantId": "variant_rst"
}
}
]
}
}Handling bundle-result events
SSE handler — bundle-result
Parse the SSE stream and handle bundle-result alongside other event types.
const response = await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
'Accept': 'text/event-stream',
},
body: JSON.stringify({
userInput: { type: 'search', value: 'my kid wants to start surfing' },
language: 'en',
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
const bundles = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
switch (event.type) {
case 'products':
renderSearchResults(event.data);
break;
case 'ranked-results':
replaceWithRankedResults(event.data);
break;
case 'summary-result':
renderSummary(event.data.summary);
break;
case 'bundle-result':
// Multiple bundle-result events may arrive (one per variant)
bundles.push(event.data);
renderBundle(event.data);
break;
case 'flow-end':
setLoading(false);
break;
case 'flow-error':
showError(event.data.message);
break;
}
}
}Bundle search (v1.4)
When a shopper describes a multi-product need, Inops assembles a curated product bundle across categories.When a shopper clicks a product, fire a similar_products flow with the clicked product's ID. Inops returns semantically similar products from your catalog, which you can render as a recommendations strip or modal.
How it works
- Shopper clicks a product card.
- Your code fires a similar_products request with the productId.
- Inops returns a ranked list of related products from your catalog.
- Render the results as "You might also like" or "Related products".
API call
similar_products request
Pass the clicked product's ID. The response streams products via SSE.
// Fire after a shopper clicks a product card
async function loadSimilarProducts(productId) {
const response = await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
'Accept': 'text/event-stream',
},
body: JSON.stringify({
userInput: { type: 'similar_products', productId },
language: 'en',
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
if (event.type === 'products') {
renderRecommendations(event.data); // "You might also like"
}
if (event.type === 'flow-end') break;
}
}
}Similar products
Surface related products when a shopper clicks on an item.Once results or a bundle are rendered, use Shopify's /cart/add.js endpoint to add items to the cart. You can add a single product or all items in a bundle in one request.
Single product
Add one product variant to cart
Use the Shopify variant ID from the product data returned by Inops.
// Add a single product variant to the Shopify cart
async function addToCart(variantId, quantity = 1) {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity }],
}),
});
const cart = await response.json();
return cart;
}Full bundle
Add all bundle items to cart at once
Map each product group in the bundle to a cart item and POST them together.
// Add all items in a bundle-result to the Shopify cart at once
async function addBundleToCart(bundle) {
// bundle.groups is the array from the bundle-result event
const items = bundle.groups.map((group) => ({
id: group.product.variantId,
quantity: 1,
}));
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
});
const cart = await response.json();
return cart;
}Add to cart
Add a single product or a full bundle to the Shopify cart.The POST /shop/flow/execute endpoint returns a Server-Sent Events (SSE) stream. Each event has a type field. Handle each type to progressively render results.
| Event type | Description |
|---|---|
| products | Initial search results (unranked). Render immediately for fast first paint. |
| unranked-products | Alias for the first batch of results before reranking is complete. |
| ranked-results | Reranked results with per-product reasons. Replace the initial results list. |
| summary-result | AI-generated summary of the search intent and top results. Display above results. |
| bundle-result | A product bundle assembled from a multi-product query. Multiple events may arrive (one per bundle variant). |
| flow-end | The pipeline is complete. No more events will follow for this request. |
| flow-error | An error occurred in the pipeline. Check the message field for details. |
Generic SSE event handler
A minimal handler that routes all event types.
async function executeFlow(userInput) {
const response = await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
'Accept': 'text/event-stream',
},
body: JSON.stringify({ userInput, language: 'en' }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
// event.type is one of: products, unranked-products, ranked-results,
// summary-result, bundle-result, flow-end, flow-error
handleEvent(event);
}
}
}
function handleEvent(event) {
switch (event.type) {
case 'products':
case 'unranked-products': renderResults(event.data); break;
case 'ranked-results': replaceResults(event.data); break;
case 'summary-result': renderSummary(event.data.summary); break;
case 'bundle-result': renderBundle(event.data); break;
case 'flow-end': setLoading(false); break;
case 'flow-error': showError(event.data.message); break;
}
}SSE events reference
All event types emitted by the /shop/flow/execute streaming endpoint.Shopify theme
Add the script in theme.liquid (head). Add the widget div where you want search.
Example (theme.liquid)
{% comment %} Inops Web SDK (head) {% endcomment %}
<script src="https://cdn.inops.io/inops-web-sdk@1.2.0/index.global.js"></script>
{% comment %} Place where you want search {% endcomment %}
<div data-widget="search" data-search-key="YOUR_SEARCH_KEY" data-id="my-search-widget"></div>WooCommerce
Add the script in your theme’s header (or via a header/footer plugin). Add the widget div to a page/template.
Example (header.php)
<!-- Inops Web SDK (head) -->
<script src="https://cdn.inops.io/inops-web-sdk@1.2.0/index.global.js"></script>
<!-- Place where you want search -->
<div data-widget="search" data-search-key="YOUR_SEARCH_KEY" data-id="my-search-widget"></div>Custom site
Add the script tag to your HTML and place the widget div where needed.
Example (HTML)
<!-- 1) Add script -->
<script src="https://cdn.inops.io/inops-web-sdk@1.2.0/index.global.js"></script>
<!-- 2) Add widget div -->
<div data-widget="search" data-search-key="YOUR_SEARCH_KEY" data-id="my-search-widget"></div>Common setups
Copy/paste examples for Shopify, WooCommerce, and a custom site.Subresource Integrity (SRI) + version pinning
Pin an exact version and add SRI so the browser rejects tampered assets.
CDN script with SRI
<script
src="https://cdn.inops.io/inops-web-sdk@1.2.0/index.global.js"
integrity="sha384-PASTE_SRI_HASH_1_0_0"
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>Content Security Policy (CSP)
Restrict scripts and API calls to only the domains you use.
Example CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.inops.io;
connect-src 'self' https://api.inops.dev;
img-src 'self' data:;
style-src 'self' 'unsafe-inline';
object-src 'none';
frame-ancestors 'none';Search Key safety
Search Keys are designed for public read‑only flows. Restrict allowed origins when possible and rotate keys regularly.
Advanced / Security
Hardening tips for production storefronts (SRI/CSP and safe key usage).Inops can cache search results for specific search terms. This is controlled via a per-term TTL (time-to-live). When a term is cached, the response is typically ~10–20ms. When a term is not cached (or the TTL has expired), the request automatically runs through the full flow (embedding + retrieval + ranking + summary), which is slower but keeps results fresh.
Cached search term
- Fast response (~10–20ms)
- Great for top queries and campaign landing pages
- Controlled by TTL per search term
Uncached / expired term
- Runs the full AI flow automatically
- Higher latency, but recomputes relevance
- Useful for long-tail queries and freshness
Configure this in the portal under Shop → Tuning → Search term TTL. You can also manually refresh cache for a term from the same screen.
Caching & TTL (search performance)
How search-term TTL affects latency and when the full AI flow runs.Capabilities
- AI search results that match shopper intent.
- Similar products to keep shoppers exploring.
- Track baskets & purchases to see what converts.
- Control ranking with boosts for products and brands (optional expiry).
For developers
- Embed a search widget with a Search Key.
- Call search and similar products via one endpoint.
- Send basket/purchase events to improve ranking and insights.
Follow-up flows (similar products, basket, purchase)
Inops uses one endpoint:
POST /shop/flow/execute. You change behavior by setting userInput.type.1) Initial search
Use X-Search-Key so you don't leak keys in URLs.
// 1) Initial search (server-side or client-side)
await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
},
body: JSON.stringify({
userInput: { type: 'search', value: 'kid longboard beginner' },
// shopConfigId is optional when using Search Key auth
language: 'en',
}),
});2) Similar products (after clicking a product)
Send the clicked productId and render the returned products as recommendations.
// 2) Similar products (after a shopper clicks a product)
await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
},
body: JSON.stringify({
userInput: { type: 'similar_products', productId: 'YOUR_PRODUCT_ID' },
language: 'en',
}),
});3) Basket event (optional)
Helps improve ranking and analytics.
// 3) Basket event (optional)
await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
},
body: JSON.stringify({
userInput: { type: 'basket', productId: 'YOUR_PRODUCT_ID' },
language: 'en',
}),
});4) Purchase event (optional)
Links search → checkout for accurate attribution and analytics.
// 4) Purchase event (optional)
await fetch('https://api.inops.dev/shop/flow/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Search-Key': 'YOUR_SEARCH_KEY',
},
body: JSON.stringify({
userInput: { type: 'purchase', productId: 'YOUR_PRODUCT_ID', orderId: 'ORDER_123' },
language: 'en',
}),
});Show API endpoints
POST /shop/flow/execute — search, similar products, basket, purchaseGET /insights/shop-config/:shopConfigId/* — insightsGET /tuning/shop-config/:shopConfigId/* — tuning (boosts, TTL)What this enables
High-level capabilities and the core endpoint a storefront uses.If the widget doesn’t show results, open DevTools → Network and find the request to
/shop/flow/execute.- 401/403: the Search Key is missing/invalid or blocked by origin restrictions.
- No requests at all: the script isn’t loading or the widget div is missing.
- CORS errors: your origin isn’t allowed (ask us to add it, or update allowlists).
Want to sanity-check quickly? Use the above.
Troubleshooting & Debugging
Fast checks when results don't show up.