A buy box drawn from a real response
In productionThis is OF_DEV_KITS as the dev API served it to a buy box: three kits, each to subscribe or to buy once, and two first-order gifts. Choose a kit or a way to buy. Every amount is one the API priced, and only options a real cart matched can be chosen.
The resolve response behind itexcerpt: the default option, ritual-set (1 of 6 options)
{
"code": "OF_DEV_KITS",
"status": "live",
"store": {
"market": {
"countryCode": "US",
"currencyCode": "USD"
},
"myshopifyDomain": "offer-suite-demo.myshopify.com"
},
"tracking": {
"campaignName": "cmp_dev_store",
"experienceName": "choose-your-kit-v1",
"presellPageName": null,
"preservedCartAttributes": []
},
"defaultOptionKey": "ritual-set",
"options": [
{
"key": "ritual-set",
"display": {
"title": "Ritual Set"
},
"visible": true,
"axisValues": {},
"unitCount": 4,
"cardKey": "ritual-set",
"lines": [
{
"role": "main",
"quantity": 1,
"required": true,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092669284719",
"variantId": "gid://shopify/ProductVariant/53479763214703"
},
"purchase": {
"type": "subscription",
"intervalDays": 30,
"sellingPlanId": "gid://shopify/SellingPlan/708367581551"
},
"catalogPrice": {
"capturedAt": "2026-09-16T00:00:00Z",
"amountCents": 8000,
"compareAtAmountCents": 12500
}
},
{
"role": "gift",
"quantity": 1,
"required": false,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092670660975",
"variantId": "gid://shopify/ProductVariant/53479764722031"
},
"purchase": {
"type": "one_time"
},
"catalogPrice": {
"capturedAt": "2026-09-16T00:00:00Z",
"amountCents": 1800,
"compareAtAmountCents": 1800
},
"claimedValueCents": 1800
},
{
"role": "gift",
"quantity": 1,
"required": false,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092670529903",
"variantId": "gid://shopify/ProductVariant/53479764623727"
},
"purchase": {
"type": "one_time"
},
"catalogPrice": {
"capturedAt": "2026-09-16T00:00:00Z",
"amountCents": 0,
"compareAtAmountCents": null
},
"claimedValueCents": 2500
}
],
"pricing": {
"promise": {
"firstChargeCents": 5860,
"renewalChargeCents": 8000
},
"compareAtPrice": {
"source": "variant",
"amountCents": 12500
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "managed_discount",
"charges": "first",
"discount": {
"per": "line",
"type": "fixed_amount",
"amountCents": 2140
},
"lineRoles": [
"main"
]
}
]
},
"prices": {
"first": {
"totalCents": 5860,
"lines": [
{
"lineIndex": 0,
"role": "main",
"variantId": "gid://shopify/ProductVariant/53479763214703",
"quantity": 1,
"sellingPlanId": "gid://shopify/SellingPlan/708367581551",
"unitCents": 8000,
"grossCents": 8000,
"discountCents": 2140,
"discountBy": "managed_discount",
"netCents": 5860
},
{
"lineIndex": 1,
"role": "gift",
"variantId": "gid://shopify/ProductVariant/53479764722031",
"quantity": 1,
"unitCents": 1800,
"grossCents": 1800,
"discountCents": 1800,
"discountBy": "gift",
"netCents": 0
},
{
"lineIndex": 2,
"role": "gift",
"variantId": "gid://shopify/ProductVariant/53479764623727",
"quantity": 1,
"unitCents": 0,
"grossCents": 0,
"discountCents": 0,
"discountBy": null,
"netCents": 0
}
]
},
"renewal": {
"totalCents": 8000,
"lines": [
{
"lineIndex": 0,
"role": "main",
"variantId": "gid://shopify/ProductVariant/53479763214703",
"quantity": 1,
"sellingPlanId": "gid://shopify/SellingPlan/708367581551",
"unitCents": 8000,
"grossCents": 8000,
"discountCents": 0,
"discountBy": null,
"netCents": 8000
}
]
},
"addOns": []
},
"checkoutMode": "direct_to_checkout",
"verification": {
"status": "matched",
"checkedAt": "2026-10-02T15:02:52.297Z"
}
}
],
"catalog": {
"gid://shopify/ProductVariant/53479764722031": {
"productId": "gid://shopify/Product/15092670660975",
"productTitle": "Gift: Mug",
"variantTitle": "Default Title",
"image": null,
"description": null,
"status": "active"
},
"gid://shopify/ProductVariant/53479763214703": {
"productId": "gid://shopify/Product/15092669284719",
"productTitle": "Ritual Set",
"variantTitle": "Default Title",
"image": null,
"description": null,
"status": "active"
},
"gid://shopify/ProductVariant/53479764623727": {
"productId": "gid://shopify/Product/15092670529903",
"productTitle": "Gift: Frother",
"variantTitle": "Default Title",
"image": null,
"description": null,
"status": "active"
}
}
}Three ways to show it
Each way serves a visitor the same version at the same server-computed prices. Only who does the work changes.
| Way | Who does it | How |
|---|---|---|
| The Verified offer theme blockIn production | The merchant, in Shopify's theme editor. No code. | App Home's setup guide opens the theme editor with the block placed on a product template. Paste the offer's id into the block's text setting and, if you like, pick an accent colour. The store's public key is written for you. |
| Your developersIn production | Your front-end team, in your own markup or a headless storefront. | GET /offers/{id}/resolve to draw the offer, POST /offers/{id}/price for any other selection, then POST /offers/{id}/cart for a checkout URL, or GET /offers/{id}/cart-input to send the cart to Shopify yourself. |
| Your agentIn production | Your agent, over MCP, with your workspace's key. | resolve_offer, price_offer_selection and create_offer_cart: the same three steps as the API, one tool each. |
$58.60
A full paid order went through the Verified offer theme block on a Plus dev store (order #1033, $58.60). On a Basic-plan dev store the block's cart reached a checkout charging the $800.00 it showed; no order was placed there.
What resolve returns
In productionOne call with the store's public key, GET /offers/{id}/resolve, returns everything a buy box draws, for the version this visitor is served or, while an experiment runs, their arm. The collapsed excerpt under the buy box above is this response, cut from the same capture.
| Structure | Axes, pools, options and their lines, with Shopify variant and selling-plan ids, tracking labels and the checkout mode. |
|---|---|
| Prices | Each line's unit, gross, discount and net price for the first order and for the steady-state renewal, compare-at prices, and gifts with their claimed value. |
| Verification | Each option's verification status and the time a real cart last checked it. |
| Catalog | Keyed by variant: product and variant titles, an image, a description excerpt and the product's status (active, draft, archived or deleted), not stock levels. |
| Caching | An ETag that changes with the version and its content, 304 on If-None-Match, and a private browser cache of 60 seconds. |
| Never returned | Costs, Shopify admin discount ids and verification evidence stay on the server. |
In the product, prices are never computed in the browser
In productionResolve already carries the price of every option on its default selection and of each optional add-on. Any other selection, such as a pick from a pool, is priced by POST /offers/{id}/price for the version that visitor is served.
Both answers come from the shared line-pricing module the publish verifier uses, which applies Shopify's per-unit floor to percentage discounts from Offer Suite's discount function. The buy box computes no price of its own: it formats the figures the API priced and, when add-ons are chosen, sums their API-priced totals.
878 carts
The shared line-pricing module the verifier uses matched what Shopify charged on 878 real dev-store carts.
Same arm everywhere
In production- Pass the visitor's id as
visitorId. While an experiment runs it pins that visitor to one arm, and resolve, price, cart and cart input all choose the version through one serving path, read on every request. - For a page rendered on your server, keep the id in a first-party
os_visitorcookie and hand the same id to the browser, so the page and the cart see the same arm. - Send the version the page rendered as
shownVersion. If the visitor is served another version by then, cart, cart input and price answer 409offer_changedwith the offer to render instead. - The theme block sends it and re-shows the offer on its own. Raw API callers and agents (create_offer_cart, get_offer_cart_input, price_offer_selection) pass
shownVersionthemselves; without it the cart is built from whatever version is served now.
Carts
In productionPOST /offers/{id}/cartbuilds the Shopify cart for the chosen option and returns its checkout URL.GET /offers/{id}/cart-inputinstead hands your code the exact CartInput the same builder makes for the publish verifier, to send to Shopify yourself.- Carts reach Shopify with a private Storefront token and the buyer's IP. Shopify's cart warnings come back unchanged, and a Storefront throttle returns 429
storefront_throttledwith Retry-After. - The verifier proves each visible option's default cart, not every selection a shopper can make.
A public key gets 120 requests a minute for each connecting IP, so one busy visitor does not throttle the store; a server rendering the buy box with a secret key can name the shopper in Offer-Suite-Buyer-IP to give each shopper their own bucket (the header is ignored from a public key). The full table is under rate limits.
Speed
In productionThe API runs beside its databases: Cloudflare placement is hinted to us-east-2, next to Postgres and ClickHouse in Ohio. Shoppers far from the US pay a longer network hop to reach it.
338 ms
Measured on the dev API from São Paulo, resolve fell from 1,389 ms to 338 ms p50 on an edge-cache miss once the API was placed beside its databases. Production latency has not been measured.
What it does not do yet
- The browser SDK is not published yet (no npm package or hosted script). Your developers call the HTTP API directly. Not available yet
- The React bindings are not on npm either. Not available yet
- The
<os-offer>element ships only inside the theme block; it is not hosted as a script of its own. Partly available - The theme block has no offer picker: the merchant pastes the offer's id, and the dashboard does not yet show that id as text to copy (it is in the offer page's address).
- Neither the theme block nor
<os-offer>draws category builders (options with several category groups or a unit cap); price those through the API. Partly available - An offer is written and published through the API, MCP or an agent before any of these can show it; App Home does not author offers.
Questions
Do I need a developer?
Not to show an offer. A merchant adds the Verified offer theme block in the theme editor and pastes the offer's id; the store's key is written automatically. Writing and publishing the offer happens through the API, MCP or an agent. Developers come in when you want your own markup, a headless storefront or a category builder. Offer Suite is not open yet: no brand has been invited, and brands join the waitlist.
Does the price on the page match what checkout charges?
Before a version goes live, each visible option's default cart is built on Shopify and must charge what the offer promises. The page's prices come from the same line-pricing module that check uses, so they are the figures the verifier expects Shopify to charge.
Can I cache the resolve response?
A browser may keep it for 60 seconds, and its ETag lets the next request come back as a 304 when nothing changed. Send the version you rendered with the cart: if the visitor is served another version by then, the cart is refused with offer_changed and the offer to render, so no cart is built for a version the page did not show.