1 min read

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. 1

    Open the Developer API page

    In Shopify admin go to Apps → StoreMind AI → Help → Developer API (the page is at /app/developer inside the app).

  2. 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-host as a placeholder; replace it with the value from that page.

  3. 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

Keys are scoped to the store that generated them — every request answers from that store’scatalog and knowledge. Running several stores? Generate a key in each store’s StoreMind admin.

Authentication#

Send the key as a Bearer token on every request (except health):

http
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 401 with { "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 APIyour 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.

request
curl https://your-app-host/api/v1/health
200 response
{ "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.

request
curl https://your-app-host/api/v1/config \
  -H "Authorization: Bearer smk_your_key"
200 response
{
  "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

SettingTypeDefaultDescription
questionstringrequiredThe shopper's question, in any language.
visitorIdstring (uuid)Stable per-user id your client generates — see The visitorId. Groups the user’s history and powers analytics + attribution. Strongly recommended.
langstringautoBCP-47 code (en, ar, hi…). Omit to auto-detect from the question.
countrystringISO country code of the shopper, for the Conversations page.
productHandlestringSet when the user is viewing a specific product — the assistant answers about that product and only suggests alternatives if asked.
productTitlestringTitle 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"
  }'
200 response
{
  "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

SettingTypeDefaultDescription
answerstringMarkdown text — product names come back as markdown links to their real store URLs. Render with any markdown renderer.
sourcesarrayThe recommended products. Render as cards: title, imageUrl, url (relative to the storefront domain), and available.
sources[].availablebooleanfalse → out of stock; show a “coming back soon” badge rather than hiding the product.
sources[].scorenumberRelevance score (0–1). Sources arrive sorted; you rarely need this.
cachedboolean?true when served from the server's short answer cache (identical repeat questions).
fallbackboolean?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:

fallback response
{
  "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.

request
curl https://your-app-host/api/v1/quiz \
  -H "Authorization: Bearer smk_your_key"
200 response
{
  "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):

submit answers
// 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.

SettingTypeDefaultDescription
rating1 | -1required1 = helpful, -1 = not helpful.
questionstringrequiredThe question that was answered.
answerstringThe reply being rated (recommended — makes thumbs-down actionable).
visitorIdstringSame id you pass to ask.
request
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"
  }'
200 response
{ "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.

SettingTypeDefaultDescription
visitorIdstringrequiredSame id you pass to ask.
events[].typestringrequired"impression" or "click".
events[].productIdstringrequiredThe product's handle (from sources[].handle) or id.
request
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" }
    ]
  }'
200 response
{ "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.

SettingTypeDefaultDescription
orderqueryrequiredThe order number — "1001" or "#1001" both work.
emailqueryrequiredThe email the order was placed with.
request
curl "https://your-app-host/api/v1/track?order=1001&email=jane@example.com" \
  -H "Authorization: Bearer smk_your_key"
200 response
{
  "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:

error shape
{ "ok": false, "error": "Invalid or missing API key" }
SettingTypeDefaultDescription
400Bad requestMissing/invalid input — e.g. no question, rating not 1/-1, body not JSON.
401UnauthorizedMissing, invalid, rotated or revoked API key.
404Not foundtrack: no order matches that number + email.
405Method not allowedWrong HTTP method for the endpoint.
500Server errorUnexpected failure. ask never 500s for AI outages — it uses the fallback contract instead.

Best practices#

  • One UUID per user, forever. Generate visitorId on first launch, persist it, and send it on every ask, feedback and eventscall — 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

Hit /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.
Last updated July 2026