All help

Storefront API: your products on your own website

The Storefront API lets your own website (a bzlabs.io site, a page you built, a theme) show your SupplySynk products and take pickup orders. It gives your site exactly what your online store shows: the same products, prices, photos and stock labels, and the same checkout with the same stock holds and emails. Orders appear in Orders with the channel "online", like orders from your store.

Who can set it up: owners and admins. Before you start: publish your store (Settings → Online store) and turn on a way to pay (Settings → Payments). An unpublished store answers "not found".

1. Create a storefront key

  1. Go to Developers → Storefront keys and click Create key.
  2. Name it after the website ("Our website").
  3. Under Websites allowed to use it, list your site's addresses, one per line, exactly as they appear in the browser's address bar without a path: https://papaswillow.com and https://www.papaswillow.com are different. While building the site on your computer, add http://localhost:3000 (only localhost may use http://).
  4. Click Create key and copy the key (pk_live_…). It's shown only once; we keep only a fingerprint of it. If you lose it, revoke it and create another.

A storefront key is safe to put in a web page. It can only read what your store already shows to everyone, and place orders the way your store does. It can't see costs, suppliers or stock counts, and it can't change anything. It doesn't work with the back-office API (/api/v1) or AI assistants; those use secret keys (Developers → API Keys), which must never go in a web page.

Websites not on the list are refused, so someone who copies your key into their own site gets nothing. Your own server (code that runs on a server, not in the browser) can always use the key, with a lower limit (see below).

To stop a website using a key, click Revoke. It stops working at once. Edit websites changes the list at once too.

2. Show your products

Every call sends the key in a header:

Authorization: Bearer pk_live_…

The address is https://www.supplysynk.com/api/storefront/v1, then:

CallWhat you get
GET /storeName, logo, colours, address, phone, email, opening hours with "Open now" or "Closed" (in your time zone), ways to pay and pickup locations
GET /categoriesCategories that have products, with counts
GET /productsProducts, 24 at a time. Add ?category=bats, ?q=bat (search), ?sort=price-asc, ?limit=48; for the next page add cursor= with the nextCursor you got
GET /products/{slug}One product: description, every photo, options and variants
GET /products/{slug}/recommendations"You may also like": up to 4 products, your pinned ones first, in stock only
GET /recommendations"Popular now" and "New arrivals" for a home page, up to 8 each
GET /pagesYour published store pages (home, About, landing pages): slug, path, title, SEO title and description
GET /pages/{slug}One published page (home, about or a landing page's slug) as sections, ready to render
POST /cartToday's prices and stock labels for a cart your site keeps
POST /checkoutPlace a pickup order
GET /orders/{token}The order's status, by the token checkout gave you

Stock shows as a label (availability.label): "In stock", "Only 3 left", "Out of stock" or "Available".

For a business in India: products and variants carry mrp (show "MRP" next to the price, struck through when the price is lower; prices include all taxes), and a product page carries legal (country of origin, the manufacturer, packer or importer with their address, net quantity, best before, other details; null when none are set). GET /store has seller: the legal name, GSTIN, grievance officer and returns policy, which an Indian online store must show. A product whose price is above its MRP has price: null, is "Not available online", is dropped from POST /cart and can't be ordered. Outside India mrp, legal and seller are null. Photos come in three sizes (image.sizes.thumb square 256 px for cards, medium up to 800 px, large up to 1600 px), with their width and height, so pages load fast on phones. Photos made by AI have generated: true; show an "AI-generated image" label with them, as your store does.

Store pages

The pages you build in Settings → Online store → Store pages come through the API as structured sections, so your own site can show the same home page or "New season bats" page as your SupplySynk store. Only published pages are returned; drafts never are. No home in GET /pages means the store uses its standard home page.

GET /pages/new-season-bats returns kind, slug, path (/p/new-season-bats), title, seoTitle, seoDescription, publishedAt and sections, in order. Each section has an id and a type:

typeFields
heroheading, subheading, image, cta (label, href)
rich_textheading, body
product_gridheading, products: the same product summaries as GET /products
category_linksheading, categories (id, name, slug, imageUrl)
image_textheading, body, image, imageSide (left or right), cta
faqheading, items (question, answer)
testimonialquote, author, detail: a customer's words, typed in by you
newsletterheading, text: show your own sign-up form here
  • body and FAQ answers use a small text format: a blank line starts a paragraph, lines starting with - are a list, **bold**, *italic* and [text](link). There is never HTML. Render it as text, or with any Markdown renderer that has raw HTML turned off.
  • Links (href) are paths in your store such as /category/bats (put your own site's address in front), #store-signup, or full https://, mailto: and tel: links.
  • Images have url (large), mediumUrl, thumbUrl, alt (always set) and generated. Product photos are always your own photos; an image with generated: true is a decorative AI image, so label it "AI-generated".
  • Products that are no longer active simply drop out of products.

The webhook store_page.published (and store_page.unpublished) tells your site when to fetch a page again. See API and webhooks.

3. Copy-paste example

Save this as a page on your site, put in your key, and open it from one of the websites you listed. It lists your products and places a "pay at pickup" order for the first one.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Our products</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 0 auto; max-width: 960px; padding: 16px; }
    ul { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
    li { border: 1px solid #ddd; border-radius: 8px; padding: 8px; }
    img { width: 100%; height: auto; aspect-ratio: 1; object-fit: cover; border-radius: 4px; }
  </style>
</head>
<body>
  <h1 id="store">Loading…</h1>
  <p id="hours"></p>
  <ul id="products"></ul>

  <form id="order" hidden>
    <h2>Order <span id="order-name"></span> for pickup</h2>
    <label>Name <input name="name" required autocomplete="name"></label><br>
    <label>Email <input name="email" type="email" required autocomplete="email"></label><br>
    <label>Phone <input name="phone" type="tel" required autocomplete="tel"></label><br>
    <button>Place order</button>
  </form>
  <p id="result"></p>

  <script>
    const API = "https://www.supplysynk.com/api/storefront/v1";
    const KEY = "pk_live_REPLACE_ME"; // Developers → Storefront keys

    async function call(path, options = {}) {
      const res = await fetch(API + path, {
        ...options,
        headers: { Authorization: "Bearer " + KEY, "Content-Type": "application/json", ...options.headers },
      });
      const body = await res.json();
      if (!res.ok) throw new Error(body.error?.message || "Something went wrong");
      return body;
    }

    let store, chosen;

    async function start() {
      store = await call("/store");
      document.getElementById("store").textContent = store.name;
      document.getElementById("hours").textContent = store.hours.status?.label ?? "";

      const { data } = await call("/products?limit=24");
      const list = document.getElementById("products");
      for (const p of data) {
        const li = document.createElement("li");
        const img = p.image
          ? `<img src="${p.image.sizes.thumb.url}" width="256" height="256" alt="" loading="lazy">`
          : "";
        li.innerHTML = `${img}<strong></strong><div class="price"></div><div class="stock"></div>`;
        li.querySelector("strong").textContent = p.name;
        li.querySelector(".price").textContent = (p.priceFrom ? "From " : "") + store.currency + " " + p.price;
        li.querySelector(".stock").textContent = p.availability.label;
        list.append(li);
      }

      // Order the first product that's in stock and isn't a family of variants
      chosen = data.find((p) => !p.hasVariants && p.availability.status !== "out_of_stock");
      if (store.checkout.enabled && chosen) {
        document.getElementById("order-name").textContent = chosen.name;
        document.getElementById("order").hidden = false;
      }
    }

    document.getElementById("order").addEventListener("submit", async (event) => {
      event.preventDefault();
      const form = new FormData(event.target);
      const result = document.getElementById("result");
      try {
        const order = await call("/checkout", {
          method: "POST",
          // The same key on a retry never places the order twice
          headers: { "Idempotency-Key": crypto.randomUUID() },
          body: JSON.stringify({
            items: [{ productId: chosen.id, quantity: 1 }],
            customer: { name: form.get("name"), email: form.get("email"), phone: form.get("phone") },
            locationId: store.checkout.locations[0].id,
            provider: store.checkout.methods[0].provider, // e.g. "pay_at_pickup"
          }),
        });
        if (order.next.kind === "redirect") {
          location.href = order.next.url; // a hosted payment page
        } else if (order.next.kind === "qr") {
          location.href = order.statusUrl; // UPI: the order page shows the QR and takes the reference
        } else {
          result.textContent = `Order ${order.orderNumber} placed. ${order.next.lines.join(" ")}`;
        }
      } catch (err) {
        result.textContent = err.message;
      }
    });

    start().catch((err) => (document.getElementById("store").textContent = err.message));
  </script>
</body>
</html>

In a Next.js site, read products on the server so the page is fast and your server makes the call (no browser origin needed), and cache it for a minute:

// app/products/page.tsx
const API = "https://www.supplysynk.com/api/storefront/v1";

export default async function Products() {
  const res = await fetch(`${API}/products?limit=48`, {
    headers: { Authorization: `Bearer ${process.env.SUPPLYSYNK_PK}` },
    next: { revalidate: 60 }, // stay under the server limit
  });
  const { data } = await res.json();
  return (
    <ul>
      {data.map((p: { id: string; name: string; price: string; availability: { label: string } }) => (
        <li key={p.id}>
          {p.name}: {p.price} ({p.availability.label})
        </li>
      ))}
    </ul>
  );
}

Place orders from the browser (a client component calling /checkout), so each shopper gets their own limit.

Checkout: what happens

  • Prices come from SupplySynk. Your site sends only product ids and quantities; any price it sends is ignored.
  • Buy a variant by its own id (variants[].id from GET /products/{slug}), not the family's id.
  • provider is one of store.checkout.methods[].provider and locationId one of store.checkout.locations[].id. Each way to pay may carry your note (note, e.g. "Ask at the counter"); show it.
  • The stock is held at the pickup location until the hold ends (48 hours for pay at pickup, unless you changed it), the shopper gets the "order received" email, and your owners and admins get a "New online order" email.
  • The response has orderNumber, total, next (what the shopper does now) and statusUrl, the shopper's order page on your store. Links in emails go there too.
  • Not enough stock: a 409 error that lists each short item. Too many tries: 429, wait and try again.

Offers opt-in at checkout

When the shop has its "Get offers" sign-up on, GET /store returns marketing: { version, email, whatsapp } (otherwise marketing: null). Show one unticked checkbox per channel with exactly that wording (the WhatsApp one only once the shopper typed a phone), make it clear they're not needed to order, and send only what was ticked:

"marketingConsent": { "email": true, "whatsapp": false, "version": "2026-10-15" }
  • A ticked box is a request, not consent. For email, the "order received" email carries a link to confirm, and one click records the consent with the wording, the time, and the IP address and browser of the checkout call (so place orders from the shopper's browser). WhatsApp waits for the shop to confirm.
  • Left out or false: nothing is recorded. With the sign-up off, it's ignored. It never affects the order, and the response doesn't say what happened.
  • version is required whenever a box is true. A missing or unknown version, or an unknown field (such as sms), is a 400.

Limits

Who callsLimit
A browser on one of your websites120 calls a minute per shopper, 3,000 a minute for the key
Your server (no Origin header)60 calls a minute for the key; cache responses
Checkout10 orders per shopper and 120 per store every 10 minutes (as in your store)

Errors look like { "error": { "code": "origin_not_allowed", "message": "…" } }. Common codes: invalid_key and key_revoked (401), origin_not_allowed (403: add the website to the key), store_not_found (404: publish the store), rate_limited (429).

The full reference for developers is at https://www.supplysynk.com/api/storefront/v1/openapi.json. More about SupplySynk's other APIs: API and webhooks.