Developers
REST API
The same assistant that powers the storefront widget, over plain HTTPS — for mobile apps, custom storefronts, kiosks, or your backend. This page documents every endpoint with real request and response shapes.
Base URL & your app host#
The API is served by the StoreMind app itself — there is no separate API domain. Your app hostand a ready-made key live in the app’s admin:
- 1
Open the Developer API page
In Shopify admin go to Apps → StoreMind AI → Help → Developer API (the page is at
/app/developerinside the app). - 2
Copy the Base URL
The Endpoints section shows your exact base URL — every path on this page is relative to it. All examples below use
https://your-app-hostas a placeholder; replace it with the value from that page. - 3
Generate your API key
Click Generate key. The key (
smk_…) is shown once — copy it immediately and store it somewhere safe (only a hash is kept server-side).
One key per store
Authentication#
Send the key as a Bearer token on every request (except health):
Authorization: Bearer smk_your_key- Rotate (Developer API page) issues a fresh key and invalidates the old one instantly; Revoke disables API access entirely.
- A missing or wrong key returns
401with{ "ok": false, "error": "Invalid or missing API key" }. - CORS is open (
*), but treat the key like a password — call the API from a backend or a compiled app, never from public web-page JavaScript where anyone can read it.
The visitorId#
Several endpoints take a visitorId. It is not fetched from the API — your client generates it: a random UUID created once per user/device and persisted, then sent on every ask, feedback and eventscall. This is exactly what the storefront widget does (it stores one in the browser’s localStorage).
// Browser — persisted in localStorage (what the widget itself does)
export function getVisitorId() {
let id = localStorage.getItem("smVisitorId");
if (!id) {
id = crypto.randomUUID(); // "b3f1c2d4-8e21-4f7a-9c11-2d55c8a0f1e3"
localStorage.setItem("smVisitorId", id);
}
return id;
}- One id per user/device, forever.Regenerating it on every launch splinters one person into many “visitors” and breaks their conversation history.
- Keep it anonymous.Use a random UUID — never an email, phone number or account id. Shoppers stay anonymous by design; the merchant’s admin shows only the random id and a generated avatar.
- What it unlocks:the visitor’s grouped history on the Conversations page, CTR/resolution analytics, and purchase attribution (the id rides along when a shopper clicks a recommended product).
- Optional but recommended.Calls work without it — the merchant just loses the per-visitor grouping for your client’s traffic.
GET /api/v1/health#
Unauthenticated connectivity check — use it for uptime monitors or an app-launch “is the service reachable” probe.
curl https://your-app-host/api/v1/health{ "ok": true, "service": "storemind", "version": "v1" }GET /api/v1/config#
The assistant’s presentation settings, so your client can mirror the storefront widget — greeting, placeholder, quick-action buttons and contact details. Fetch it once at startup and cache it for the session.
curl https://your-app-host/api/v1/config \
-H "Authorization: Bearer smk_your_key"{
"ok": true,
"shop": "your-store.myshopify.com",
"config": {
"heroTitle": "Hey there — what can we help with?",
"inputPlaceholder": "Ask about products…",
"quickActionsJson": "[{ \"id\": \"deals\", \"label\": \"Latest deals\", … }]",
"contactIntro": "We're here to help.",
"contactPhone": "+973 17212698",
"contactWhatsapp": "+973 17212698",
"contactEmail": "hello@your-store.com"
}
}quickActionsJson is a JSON string (parse it) of { id, label, type, title, prompt?, category? } buttons — render them as shortcuts that send their prompt through ask.
POST /api/v1/ask#
The main endpoint: ask anything, get an answer grounded in the store’s catalog and knowledge, plus the products it recommended.
Request body
| Setting | Type | Default | Description |
|---|---|---|---|
question | string | required | The shopper's question, in any language. |
visitorId | string (uuid) | — | Stable per-user id your client generates — see The visitorId. Groups the user’s history and powers analytics + attribution. Strongly recommended. |
lang | string | auto | BCP-47 code (en, ar, hi…). Omit to auto-detect from the question. |
country | string | — | ISO country code of the shopper, for the Conversations page. |
productHandle | string | — | Set when the user is viewing a specific product — the assistant answers about that product and only suggests alternatives if asked. |
productTitle | string | — | Title of that product (pairs with productHandle). |
curl -X POST https://your-app-host/api/v1/ask \
-H "Authorization: Bearer smk_your_key" \
-H "Content-Type: application/json" \
-d '{
"question": "Do you ship internationally?",
"visitorId": "b3f1c2d4-8e21-4f7a-9c11-2d55c8a0f1e3",
"lang": "en",
"country": "IN"
}'{
"ok": true,
"answer": "Yes, we ship internationally! We offer Standard International Delivery…",
"lang": "en",
"sources": [
{
"title": "Activus Homme 100ML",
"handle": "activus-homme-100ml",
"url": "/products/activus-homme-100ml",
"imageUrl": "https://cdn.shopify.com/s/files/…/activus.jpg",
"productId": "gid://shopify/Product/9779909656800",
"score": 0.87,
"available": true
}
]
}Response fields
| Setting | Type | Default | Description |
|---|---|---|---|
answer | string | — | Markdown text — product names come back as markdown links to their real store URLs. Render with any markdown renderer. |
sources | array | — | The recommended products. Render as cards: title, imageUrl, url (relative to the storefront domain), and available. |
sources[].available | boolean | — | false → out of stock; show a “coming back soon” badge rather than hiding the product. |
sources[].score | number | — | Relevance score (0–1). Sources arrive sorted; you rarely need this. |
cached | boolean? | — | true when served from the server's short answer cache (identical repeat questions). |
fallback | boolean? | — | true when the AI service was briefly unavailable — see below. |
The fallback contract
If the AI service is down or rate-limited, ask still returns 200 / ok: true — with a calm, customer-safe message and fallback: true. Show it like a normal reply, but never cache it, and retry normally on the next question:
{
"ok": true,
"answer": "We're taking a short break right now and can't answer just yet — please try again in a few minutes. …",
"sources": [],
"fallback": true,
"lang": "en"
}GET /api/v1/quiz#
The store-specific product-finder quiz. Show one question at a time; multiSelect tells you whether multiple options can be chosen.
curl https://your-app-host/api/v1/quiz \
-H "Authorization: Bearer smk_your_key"{
"ok": true,
"questions": [
{
"id": "q1",
"question": "Who are you shopping for today?",
"multiSelect": false,
"options": [
{ "label": "Myself", "value": "myself" },
{ "label": "A gift", "value": "a gift" },
{ "label": "Just browsing", "value": "just browsing" }
]
},
{
"id": "q2",
"question": "What matters most to you?",
"multiSelect": true,
"options": [
{ "label": "Best value", "value": "best value" },
{ "label": "Premium quality", "value": "premium quality" }
]
}
]
}Submitting the answers
There’s no separate submit endpoint — compose the answers into a single ask question (this is exactly what the widget does):
// Compose the answers into one ask() question — exactly what the widget does:
const summary = answers
.map(a => `${a.question}: ${a.labels.join(", ")}`)
.join(". ");
await fetch("https://your-app-host/api/v1/ask", {
method: "POST",
headers: {
"Authorization": "Bearer smk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
question:
"A customer completed a product-finder quiz. Their preferences: " +
summary + ". Recommend the best matching products.",
visitorId,
}),
});POST /api/v1/feedback#
Thumbs-up / thumbs-down on a reply. Ratings feed the store owner’s monthly feedback digest, so wire your 👍/👎 buttons here.
| Setting | Type | Default | Description |
|---|---|---|---|
rating | 1 | -1 | required | 1 = helpful, -1 = not helpful. |
question | string | required | The question that was answered. |
answer | string | — | The reply being rated (recommended — makes thumbs-down actionable). |
visitorId | string | — | Same id you pass to ask. |
curl -X POST https://your-app-host/api/v1/feedback \
-H "Authorization: Bearer smk_your_key" \
-H "Content-Type: application/json" \
-d '{
"rating": -1,
"question": "Do you ship internationally?",
"answer": "The answer the shopper rated…",
"visitorId": "b3f1c2d4-8e21-4f7a-9c11-2d55c8a0f1e3"
}'{ "ok": true }POST /api/v1/events#
Product impressions and clicks — they power the store’s Recommendation CTR and AI resolution metrics. Send an impression for every product card you render and a click when the user taps one. Batch them per reply.
| Setting | Type | Default | Description |
|---|---|---|---|
visitorId | string | required | Same id you pass to ask. |
events[].type | string | required | "impression" or "click". |
events[].productId | string | required | The product's handle (from sources[].handle) or id. |
curl -X POST https://your-app-host/api/v1/events \
-H "Authorization: Bearer smk_your_key" \
-H "Content-Type: application/json" \
-d '{
"visitorId": "b3f1c2d4-8e21-4f7a-9c11-2d55c8a0f1e3",
"events": [
{ "type": "impression", "productId": "activus-homme-100ml" },
{ "type": "impression", "productId": "ward-spray-100ml" },
{ "type": "click", "productId": "activus-homme-100ml" }
]
}'{ "ok": true }GET /api/v1/track#
Guest-safe order lookup. Both parameters are required — order numbers alone are sequential and guessable, so the email acts as the shared secret that proves ownership.
| Setting | Type | Default | Description |
|---|---|---|---|
order | query | required | The order number — "1001" or "#1001" both work. |
email | query | required | The email the order was placed with. |
curl "https://your-app-host/api/v1/track?order=1001&email=jane@example.com" \
-H "Authorization: Bearer smk_your_key"{
"ok": true,
"order": {
"name": "#1001",
"processedAt": "2026-07-12T09:14:03Z",
"fulfillmentStatus": "FULFILLED",
"financialStatus": "PAID",
"total": { "amount": "48.0", "currencyCode": "USD" }
}
}No match returns 404 with { "ok": false, "error": "No order found for that order number + email" }— show it as “check your order number and email” rather than a hard error.
Errors#
Every error is JSON with ok: false and a human-readable error:
{ "ok": false, "error": "Invalid or missing API key" }| Setting | Type | Default | Description |
|---|---|---|---|
400 | Bad request | — | Missing/invalid input — e.g. no question, rating not 1/-1, body not JSON. |
401 | Unauthorized | — | Missing, invalid, rotated or revoked API key. |
404 | Not found | — | track: no order matches that number + email. |
405 | Method not allowed | — | Wrong HTTP method for the endpoint. |
500 | Server error | — | Unexpected failure. ask never 500s for AI outages — it uses the fallback contract instead. |
Best practices#
- One UUID per user, forever. Generate
visitorIdon first launch, persist it, and send it on everyask,feedbackandeventscall — it ties the whole journey together in the merchant’s analytics. - Keep the key server-side. In a mobile app, proxy through your backend if you can; never embed the key in public web JS.
- Cache
configper session; don’t refetch per message. - Respect
fallback: true— display it, don’t cache it, and consider a short client-side cool-down before retrying. - Render markdown. Answers use links, bold and lists — plain-text rendering loses the product links.
Test in one minute
/api/v1/health with no auth to confirm connectivity, then run the ask example with your real key — the Developer API page in the app shows a copy-paste curl preloaded with your base URL.