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
/headless/*. Every request identifies the
target store with a shop query parameter:
Prerequisites
- Install Knit. See Getting started.
- Configure at least one active bundle.
- 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.
Endpoint 1 — Bundle Data
Query parameters
Example
Response
Caching
Successful responses are sent with:Endpoint 2 — Embed (iframe)
<iframe> and you’re done rendering — you only need to handle
add-to-cart (see postMessage bridge).
Query parameters
Usage
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 overwindow.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
/headless/bundles response above):
Notes:
selectorIddiffers per line when the bundle has multiple selectors. The full string is therefore often not identical across lines — onlyatcId(and usuallybundleId/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:
-
Resolve which
conditionSetthe shopper’s selection satisfies (e.g. quantity 2 → the set whosequantitycondition hasminimum/maximum2). -
For each cart line that belongs to a selector referenced by that set, set:
-
Use one
atcIdfor every line in that single add-to-cart call.
157):
How to verify
- Add a qualifying bundle to cart and open the cart (or place a test order).
- Confirm each discounted line shows a line-item property / attribute named
__FB_ATC_UIDwith a 5-part|-delimited value. - Confirm the Knit automatic discount is Active in Shopify admin → Discounts, and the bundle is Active in Knit.
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
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
conditionSetfires when itsconditionsare met for the referenced selector(s) — e.g. aquantitycondition withminimum: 2, maximum: 2fires when the shopper picks quantity 2. - The matching
rewardSet.rewardsthen apply — e.g.percentageDiscountof 19% off, or afixedDiscountamount.
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 tohttps://{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 yourshop domain, the endpoint URL, and the full JSON error response
(or the [nameless] console logs from the embed iframe) when contacting support.