Skip to main content

Knit Bundles — Headless Integration Guide

Knit Bundles exposes public endpoints for headless storefronts, mobile apps, and custom integrations that don’t run on the standard Shopify Liquid theme. There are two ways to integrate, depending on how much control you want: Both modes are served from the Knit app origin, not from your myshopify.com storefront. This is deliberate — see Why the app origin? below.

Base URL

All headless endpoints live under /headless/*. Every request identifies the target store with a shop query parameter:

Prerequisites

  1. Install Knit. See Getting started.
  2. Configure at least one active bundle.
  3. Open the app once in the Shopify admin — this provisions the Storefront access token the headless endpoints need. Until this happens, the endpoints return APP_NOT_INSTALLED.
You do not need to embed the theme app extension for the headless endpoints to work.

Endpoint 1 — Bundle Data

Returns all active bundles for the shop as fully normalized JSON: resolved products, typed conditions and rewards, and the assets (JS/CSS) associated with each bundle. Use this when you want to build your own bundle UI.

Query parameters

Example

Response

Caching

Successful responses are sent with:
Bundle changes may take up to ~5 minutes to propagate through caches.

Endpoint 2 — Embed (iframe)

Returns a self-contained HTML page that loads the Knit widget runtime, fetches the applicable bundles, and renders the bundle widget for the given product. Drop it into an <iframe> and you’re done rendering — you only need to handle add-to-cart (see postMessage bridge).

Query parameters

Usage

The embed page is served with Cache-Control: no-store and without X-Frame-Options / frame-ancestors, so it is framable from any origin.

postMessage bridge

The embed iframe communicates with your host page over window.postMessage. It renders the widget and manages selection state itself, but it cannot add to your cart — that’s your job. You must listen for the messages below. Every message includes source, v (version), and type. Ignore any message whose source/v you don’t recognize.

Messages from iframe → your page

source: "nameless-bundle", v: 1

Messages from your page → iframe

source: "nameless-bundle-host", v: 1

lineItems shape

Cart attribute required for discount (__FB_ATC_UID)

Bundle discounts are applied by the Knit Discount Function (a Shopify Function on cart.lines.discounts.generate.run). It does not infer bundles from products alone. It only discounts lines that carry the line-item property: If this property is missing, renamed, or stripped by your cart layer, add-to-cart succeeds but no bundle discount appears at checkout.

Value format

Example (matches the sample /headless/bundles response above):
Notes:
  • selectorId differs per line when the bundle has multiple selectors. The full string is therefore often not identical across lines — only atcId (and usually bundleId / version / conditionSetId) is shared.
  • Lines with no satisfied condition set should omit __FB_ATC_UID (they are ordinary cart lines; the function ignores them for bundling).
  • Do not invent IDs. Use the numeric string IDs from the Data API / embed payload as-is (e.g. "72", not a GID).

Embed mode (iframe)

The iframe already stamps __FB_ATC_UID on each lineItems[].attributes entry. Your only job is to forward those attributes unchanged into Shopify: Do not filter “hidden” / underscore-prefixed properties. Many cart SDKs drop them by default — that is the most common cause of “items add, discount missing”.

Data API mode (custom UI)

When you build your own UI from /headless/bundles, you must set the property yourself when writing to the cart:
  1. Resolve which conditionSet the shopper’s selection satisfies (e.g. quantity 2 → the set whose quantity condition has minimum/maximum 2).
  2. For each cart line that belongs to a selector referenced by that set, set:
  3. Use one atcId for every line in that single add-to-cart call.
Pseudo-example for the sample bundle (Buy 2 → 19% off on selector 157):

How to verify

  1. Add a qualifying bundle to cart and open the cart (or place a test order).
  2. Confirm each discounted line shows a line-item property / attribute named __FB_ATC_UID with a 5-part |-delimited value.
  3. Confirm the Knit automatic discount is Active in Shopify admin → Discounts, and the bundle is Active in Knit.
If the property is present but the discount still does not apply, the conditionSetId / quantities likely no longer match the live bundle definition (stale cart after a merchant edit), or the automatic discount was disabled.

Minimal host handler

If you don’t reply to an addToCart within ~10 seconds, the widget resolves the action as failed (CART_TIMEOUT) and shows an error to the shopper.

Response schema reference

The /headless/bundles endpoint returns { ok: true, bundles: Bundle[] }. Each Bundle has this shape (TypeScript):

How pricing works

Bundle discounts are expressed as conditions → rewards, not as precomputed prices:
  • A conditionSet fires when its conditions are met for the referenced selector(s) — e.g. a quantity condition with minimum: 2, maximum: 2 fires when the shopper picks quantity 2.
  • The matching rewardSet.rewards then apply — e.g. percentageDiscount of 19% off, or a fixedDiscount amount.
To display “Buy 2, save 19%” tiers, map each conditionSet to its exact quantity (minimum === maximum) and read the reward for that selector. See Renderer function for a complete implementation pattern. Display prices are not enough to get a discount at checkout. When you add the qualifying lines to the cart, stamp __FB_ATC_UID as described in Cart attribute required for discount. Without that line-item property, Shopify never applies the Knit discount.

Error responses

Errors return { ok: false, code, message } with an HTTP status: The /headless/embed endpoint returns plain-text 400 / 404 bodies for the same missing-param / not-enabled conditions.

Why the app origin?

Headless clients can’t frame {shop}.myshopify.com directly: Shopify sends Content-Security-Policy: frame-ancestors 'none' on every storefront response, and it isn’t merchant-configurable. So the embed page is served from the Knit app origin, which sets no such header and is therefore framable anywhere. Because the page lives on the app origin (not the storefront), the widget fetches its data from /headless/bundles (same-origin, no CORS) rather than the storefront App Proxy.

Note on the App Proxy endpoints

You may see references to https://{shop}.myshopify.com/apps/proxy/bundles and /apps/proxy/embed. Those are HMAC-signed App Proxy endpoints intended for hosts that can frame the storefront origin. They return the same normalized Bundle[] schema documented above. For headless apps and mobile/WebView hosts, use the /headless/* endpoints in this guide instead.

Support

Include your shop domain, the endpoint URL, and the full JSON error response (or the [nameless] console logs from the embed iframe) when contacting support.