One document, every structure
In productionAn offer is one strict JSON document in the offer/1 format. There is no list of offer types to pick from: a bundle, a subscription, a gift ladder and a build-your-own box are the same few primitives arranged differently. Options say what a buyer can choose, lines with a main, add-on or gift role say what goes in the cart, and each option carries its own pricing and shipping.
Your agent writes the document through the API (POST /offers, PUT /offers/{id}) or the MCP tools built from it, create_offer and replace_offer.
| Structure | What it does | Document key |
|---|---|---|
| Quantity tiers | One option per tier (1 bag, 2 bags, 4 bags), each with the price it promises the buyer. | options, unitCount, pricing.promise |
| Per-unit tiers | In a category builder, a group's first-order unit price falls as the cart holds more units of it; the discount function charges it at checkout. | unit_price_tiers |
| Subscribe and save | Lines bought on the selling plans you already sell through, which can share a card with a one-time twin (cardKey). In a category builder, a group's plan can step by its units (purchase.plans). | purchase.sellingPlanId, purchase.plans, cardKey |
| Build your own | The buyer picks from a minimum to a maximum number of units from a pool of variants, in any mix, with repeats allowed or not. | pools, variantSource.type: pool, quantity, maxQuantity |
| Add-ons | Optional lines a cart holds only when the request names their key. | role: add_on, required: false, key |
| Free gifts and gift ladders | Gift lines are real products the discount function prices to zero, only up to the units the option includes. Each option carries its own gifts, so a buy box can show a larger option's gifts as the locked next step. | role: gift |
| Axes | Flavour, size or grind: each option names its value on each axis, and a line's variant follows those values. | axes, axisValues, variantSource.type: axis_map |
| Free shipping | Per option: the cheapest rate, every rate, or rates named by title, on the first order only or on every renewal too. | pricing.shipping, rates, charges |
| Checkout destination | Where the page sends the buyer after the choice: the cart page, a cart drawer, straight to checkout, a cart on the landing page, or an external flow. | checkoutMode |
| Value claims | Bonuses and stated values the page shows. They are copy only and never enter the cart. | valueClaims |
OF_DEV_KITS, the dev store's offeroffer/1, six options
{
"schemaVersion": "offer/1",
"code": "OF_DEV_KITS",
"tracking": {
"experienceName": "choose-your-kit-v1",
"campaignName": "cmp_dev_store",
"presellPageName": null,
"preservedCartAttributes": []
},
"store": {
"myshopifyDomain": "offer-suite-demo.myshopify.com",
"market": {
"countryCode": "US",
"currencyCode": "USD"
}
},
"axes": [],
"pools": [],
"defaultOptionKey": "ritual-set",
"options": [
{
"key": "single-bag",
"display": {
"title": "Single Bag"
},
"visible": true,
"axisValues": {},
"unitCount": 1,
"cardKey": "single-bag",
"lines": [
{
"role": "main",
"quantity": 1,
"required": true,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092668957039",
"variantId": "gid://shopify/ProductVariant/53479762788719"
},
"purchase": {
"type": "subscription",
"sellingPlanId": "gid://shopify/SellingPlan/708367581551",
"intervalDays": 30
},
"catalogPrice": {
"amountCents": 4000,
"compareAtAmountCents": 5000,
"capturedAt": "2026-09-16T00:00:00Z"
}
}
],
"pricing": {
"promise": {
"firstChargeCents": 3200,
"renewalChargeCents": 4000
},
"compareAtPrice": {
"amountCents": 5000,
"source": "variant"
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "managed_discount",
"lineRoles": [
"main"
],
"discount": {
"type": "fixed_amount",
"amountCents": 800,
"per": "line"
},
"charges": "first"
}
]
},
"checkoutMode": "direct_to_checkout"
},
{
"key": "single-bag-once",
"display": {
"title": "Single Bag"
},
"visible": true,
"axisValues": {},
"unitCount": 1,
"cardKey": "single-bag",
"lines": [
{
"role": "main",
"quantity": 1,
"required": true,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092668957039",
"variantId": "gid://shopify/ProductVariant/53479762788719"
},
"purchase": {
"type": "one_time"
},
"catalogPrice": {
"amountCents": 4000,
"compareAtAmountCents": 5000,
"capturedAt": "2026-09-16T00:00:00Z"
}
}
],
"pricing": {
"promise": {
"firstChargeCents": 4000,
"renewalChargeCents": null
},
"compareAtPrice": {
"amountCents": 5000,
"source": "variant"
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "catalog_price"
}
]
},
"checkoutMode": "direct_to_checkout"
},
{
"key": "starter-kit",
"display": {
"title": "Starter Kit"
},
"visible": true,
"axisValues": {},
"unitCount": 2,
"cardKey": "starter-kit",
"lines": [
{
"role": "main",
"quantity": 1,
"required": true,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092669088111",
"variantId": "gid://shopify/ProductVariant/53479763018095"
},
"purchase": {
"type": "subscription",
"sellingPlanId": "gid://shopify/SellingPlan/708367581551",
"intervalDays": 30
},
"catalogPrice": {
"amountCents": 6000,
"compareAtAmountCents": 8000,
"capturedAt": "2026-09-16T00:00:00Z"
}
},
{
"role": "gift",
"quantity": 1,
"required": false,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092670660975",
"variantId": "gid://shopify/ProductVariant/53479764722031"
},
"purchase": {
"type": "one_time"
},
"catalogPrice": {
"amountCents": 1800,
"compareAtAmountCents": 1800,
"capturedAt": "2026-09-16T00:00:00Z"
},
"claimedValueCents": 1800
}
],
"pricing": {
"promise": {
"firstChargeCents": 4800,
"renewalChargeCents": 6000
},
"compareAtPrice": {
"amountCents": 8000,
"source": "variant"
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "managed_discount",
"lineRoles": [
"main"
],
"discount": {
"type": "fixed_amount",
"amountCents": 1200,
"per": "line"
},
"charges": "first"
}
]
},
"checkoutMode": "direct_to_checkout"
},
{
"key": "starter-kit-once",
"display": {
"title": "Starter Kit"
},
"visible": true,
"axisValues": {},
"unitCount": 2,
"cardKey": "starter-kit",
"lines": [
{
"role": "main",
"quantity": 1,
"required": true,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092669088111",
"variantId": "gid://shopify/ProductVariant/53479763018095"
},
"purchase": {
"type": "one_time"
},
"catalogPrice": {
"amountCents": 6000,
"compareAtAmountCents": 8000,
"capturedAt": "2026-09-16T00:00:00Z"
}
}
],
"pricing": {
"promise": {
"firstChargeCents": 6000,
"renewalChargeCents": null
},
"compareAtPrice": {
"amountCents": 8000,
"source": "variant"
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "catalog_price"
}
]
},
"checkoutMode": "direct_to_checkout"
},
{
"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",
"sellingPlanId": "gid://shopify/SellingPlan/708367581551",
"intervalDays": 30
},
"catalogPrice": {
"amountCents": 8000,
"compareAtAmountCents": 12500,
"capturedAt": "2026-09-16T00:00:00Z"
}
},
{
"role": "gift",
"quantity": 1,
"required": false,
"variantSource": {
"type": "fixed",
"productId": "gid://shopify/Product/15092670660975",
"variantId": "gid://shopify/ProductVariant/53479764722031"
},
"purchase": {
"type": "one_time"
},
"catalogPrice": {
"amountCents": 1800,
"compareAtAmountCents": 1800,
"capturedAt": "2026-09-16T00:00:00Z"
},
"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": {
"amountCents": 0,
"compareAtAmountCents": null,
"capturedAt": "2026-09-16T00:00:00Z"
},
"claimedValueCents": 2500
}
],
"pricing": {
"promise": {
"firstChargeCents": 5860,
"renewalChargeCents": 8000
},
"compareAtPrice": {
"amountCents": 12500,
"source": "variant"
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "managed_discount",
"lineRoles": [
"main"
],
"discount": {
"type": "fixed_amount",
"amountCents": 2140,
"per": "line"
},
"charges": "first"
}
]
},
"checkoutMode": "direct_to_checkout"
},
{
"key": "ritual-set-once",
"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": "one_time"
},
"catalogPrice": {
"amountCents": 8000,
"compareAtAmountCents": 12500,
"capturedAt": "2026-09-16T00:00:00Z"
}
}
],
"pricing": {
"promise": {
"firstChargeCents": 8000,
"renewalChargeCents": null
},
"compareAtPrice": {
"amountCents": 12500,
"source": "variant"
},
"shipping": {
"type": "free",
"rates": {
"type": "cheapest"
},
"charges": "every"
},
"mechanisms": [
{
"type": "catalog_price"
}
]
},
"checkoutMode": "direct_to_checkout"
}
],
"valueClaims": [],
"terms": {},
"costProfile": "dtc-us"
}Three of the structures above are in it: quantity tiers of 1, 2 and 4 units, each subscription option paired with a one-time twin on the same card (cardKey), and free gift lines on the subscription options of the two larger kits. Every option ships free on the cheapest rate and sends the buyer straight to checkout.
Validation that names the path
In productionThe API checks every create and replace against the schema. A wrong document is refused with 400 invalid_document and one issue per problem, each with the path to the field and what is wrong, so an agent can fix it without guessing. Nothing is stored and nothing reaches Shopify.
| Path | What the schema says | Dev API |
|---|---|---|
options[4] | a subscription purchase requires sellingPlanId | Same issue |
defaultOptionKey | defaultOptionKey "ritual" names no option in this document | Same issue |
What the dev API returnedPOST /offers, 400, dev API, 2 October 2026
{
"error": {
"code": "invalid_document",
"message": "The offer document is invalid",
"issues": [
{
"path": "options[4].lines[0].purchase.sellingPlanId",
"message": "a subscription purchase requires sellingPlanId"
},
{
"path": "defaultOptionKey",
"message": "defaultOptionKey \"ritual\" names no option in this document"
}
]
}
}Prices Shopify does not show as a discount
In productionLine price. Sell a one-time, non-gift line at or below the catalog price captured in the document, as the product's own price: there is no strikethrough and no discount line at checkout. A higher price is refused when the document is validated. Subscription lines, gift lines and lines already priced by a bundle or tier group cannot take one. If the catalog later falls to or below the line price, the catalog price stays and the verifier reports it.
Presentations. Offer Suite's own cart transform can change how an option's one-time lines look and price in the cart and at checkout:
- Line override: one line shown under another title or image, in the cart and at checkout only; the order keeps the real title. Shopify Plus only.
- Merge: several lines shown as one parent line, a bundle product that cannot be bought on its own.
- Expand: one line shown as its components, each with its own price and role, on every plan.
The real variants still ship. On the order they arrive as a Shopify bundle group carrying each component's _os_role, so the ingest costs every component to its option.
Store capability. Each store's capability, its plan and whether the cart transform is registered, is read at install, on shop and scope changes, and daily, and served at GET /stores/{id}. A publish refuses a document the store cannot run with 409 store_capability_missing, naming each option and operation.
| Operation | Shopify Plus | Other plans | Development store |
|---|---|---|---|
| Line price | Runs | Runs, as an expand | Runs |
| Line override (title or image) | Runs | Refused at publish | Runs on Plus dev stores; refused on others |
| Merge into a parent line | Runs | Runs | Runs |
| Expand into priced components | Runs | Runs | Runs |
Subscriptions on your selling plans
In productionBind the selling plans you already sell through, from apps that use Shopify selling plans such as Recharge or Skio. Renewal orders have been verified with Shopify's own subscription contracts; renewals from those apps are not confirmed yet (tracking). Offer Suite puts the price on two app discounts:
- First-charge pricing goes on the Offer Suite app discount, which applies once.
- Recurring discounts and free shipping on every shipment go on Offer Suite renewals, which Shopify saves on the subscription contract for every billing cycle.
- The schema refuses a mechanism that would price one line on both, because Shopify applies one product discount per line.
At publish, a real cart and a read of the live discounts check the selling plan, the first charge, the renewal price and that the renewals discount is active on every cycle. No renewal order is placed for the check. Publish and verify has every check.
Drafts and versions
In productionEvery replace stores draft n+1 with an audit row, and the live version keeps serving. A create or a replace never touches Shopify: only an explicit publish does, and it flips the live version only after real carts charge what the document promises.
| Draft | Written by a create or a replace, or a version whose publish did not match. Nothing serves it. |
|---|---|
| Verified | Published as an experiment arm: proved on real carts and served only to the visitors an experiment assigns to it. |
| Live | The version every other visitor is served. |
| Retired | Replaced by a newer live version. Kept as it was, never edited. |
Storefront code cannot change an offer. The public key a buy box holds can resolve the live offer, price a selection, build a cart and post events; it cannot write an offer.
Written for agents
In productionEvery property of the offer document, the resolve response and the event body carries a description, and a test fails any new field without one. A second test checks the same schemas on the served /openapi.json.
The hosted MCP server builds create_offer and replace_offer from /openapi.json at runtime, so an agent reads those descriptions field by field and can write an offer from the schema alone.
unitCount | The quantity the buyer perceives ("4 units"); not the sum of line quantities |
|---|---|
cardKey | Options sharing a card key are drawn as one card (a subscribe and one-time pair of one product) |
checkoutMode | Where the brand's page sends the buyer after the choice: the cart page, a cart drawer, straight to checkout, a cart on the landing page, or an external flow |
1 call
In an internal test in September 2026, a fresh agent with only the MCP resources wrote a valid offer on its first create_offer call.
Checking stored documents
In productionGET /offers/document-check (check_offer_documents) validates every offer's live version, current version and verified experiment arms against today's schema, and lists each failing version with the path and message of every issue.
A repair is a new draft written with PUT /offers/{id} and a reason. That write records an audit row naming the versions it supersedes, and the new version then goes through a normal publish. A stored version is never edited in place, and the failing one keeps serving until its replacement is live.
Category builders
Partly availablePriced and verified through the API, with no ready-made builder UI yet.
An option can be a category builder: optional groups, each one category bought on subscription or once, with an overall unit cap (maxUnits) and one cadence set by the units in the largest subscription group (cadence). Each group's first-order unit price falls as the cart holds more of it (unit_price_tiers), and renewals are priced by the brand's own selling plans, bound per tier and cadence.
POST /offers/{id}/price (price_offer_selection) prices any selection, cart creation builds a matching Shopify cart, and the publish verifier checks both.
48 of 48
One category-builder offer had 48 of 48 options matched and 377 of 377 audit carts, and test order #1023 charged $154.06.
Your developers build the builder's UI. Nothing Offer Suite ships draws one yet: the Verified offer theme block draws an offer's options, not a builder's groups.
What it does not do yet
- Descriptions are enforced for the offer document, the resolve response and the event body, not for every query parameter.
- Free-shipping checks run only for US-market stores. Elsewhere the check is skipped and the version is not published.
- Line overrides need Shopify Plus.
- Category builders have no ready-made UI.
Questions
Is there a list of offer types?
No. An offer is options and lines with a main, add-on or gift role, plus pricing and shipping. Tiers, bundles, subscriptions, gift ladders and build-your-own boxes are arrangements of those few primitives, so a new structure needs no new type.
What happens when a document is wrong?
The API refuses it with 400 invalid_document and one issue per problem, each naming the field's path and what is wrong. Nothing is stored and nothing reaches Shopify, so your agent fixes the fields and sends it again.
Can I change a live offer?
Not in place. A replace stores a new draft while the live version keeps serving, and the change goes live only when that draft is published and its real carts charge what it promises.