Headless & custom storefronts
This page is the integration contract for a storefront that isn’t a Liquid theme — Hydrogen, a custom React front end, a native app, or anything else talking to Shopify’s Storefront API. It says what your cart must send, and what happens when it sends it wrong.
A headless storefront can create Curobi subscriptions today
With no code from Curobi, and no Curobi credential.
Subscriptions on Shopify are selling plans. A cart line carrying a sellingPlanId produces a
checkout that Shopify turns into a SubscriptionContract, which fires orders/create, which Curobi
mirrors on exactly the same path a Liquid theme uses. Nothing in that webhook knows or cares whether
a theme app extension was involved.
Everything below is about the extras — build-a-box, Mix & Match, gifts. Those ride on cart-line properties, and they’re the part that fails silently.
Get a property key wrong and nothing errors. The add-to-cart succeeds, checkout succeeds, the subscription is created, the order ships — and the feature simply never happens. No box selection is recorded. No gift is captured. No discount group is formed.
There is no runtime validation that can catch this for you: by the time the order reaches us, a wrong key is indistinguishable from the merchant not having used the feature. Copy the keys below exactly, then place a real test order and confirm it appears in the Curobi admin.
Cart-line properties
Written at add-to-cart, carried through checkout onto the order line’s customAttributes, and read
there by Curobi.
| Key | Feature | Value | Visible to buyer |
|---|---|---|---|
_rc_box_pick | Build-a-box | variantGid=qty segments, comma-joined | no |
Your picks | Build-a-box | Human-readable companion, e.g. Coffee ×2, Tea | yes |
_rc_mm_group | Mix & Match | The plan’s cart_group_id | no |
_rc_gift | Gift | Any truthy value — 1, true, yes | no |
_rc_gift_email | Gift | Recipient email — required | no |
_rc_gift_start | Gift | Optional YYYY-MM-DD first delivery | no |
Gift recipient | Gift | Recipient name | yes |
Gift message | Gift | The gifter’s note | yes |
The leading _ is Shopify’s convention for hiding a property from the buyer-facing order summary.
The unprefixed keys are visible on purpose: who the gift is for, the gift note and the box
contents are exactly what a merchant wants on the order and the packing slip. Preserve that split —
it’s part of the contract, not an accident of naming. A headless build that hides all eight produces
orders the fulfilment team can’t read.
These keys are add-only. Renaming one would orphan in-flight carts, so they won’t change.
Build-a-box
_rc_box_pick carries the picks:
gid://shopify/ProductVariant/44000000000001=2,gid://shopify/ProductVariant/44000000000002=1- Segments joined by
,; variant and quantity separated by=. Neither character appears in a variant GID, so the split is unambiguous. - A bare GID with no
=qtyis treated as quantity 1 — a legacy format, still accepted. - Quantity is clamped server-side to 1–10 per item, and a repeated variant is summed then re-clamped.
- Use the GID, not the numeric id. The menu metafield gives you GIDs; pass them through unchanged.
The server re-validates every pick against the published menu and the plan’s min/max bounds, so a crafted payload can’t smuggle in an item that isn’t on the menu. Invalid picks are dropped, not rejected — another reason to test with a real order.
Mix & Match
_rc_mm_group is what groups the lines of one selection.
- Every line of one selection must carry the same value.
- The value is the plan’s
cart_group_id— available from themix_matchproduct metafield asgroupId, or fromGET /api/admin/v1/mix-match/{id}ascart_group_id. - Set
selling_planon each line only when the shopper chose to subscribe. Omit it for one-time purchases. - Don’t put the group id on lines that aren’t part of the selection.
The tier discount is applied by a Shopify Product Discount Function that runs server-side at
checkout and re-derives the tier from real cart quantities. So the discount lands whether the
picker was Liquid, React or curl, and a crafted cart can’t claim a richer tier than it bought.
Gifts
_rc_gift_email is mandatory. A gift line without a plausible recipient email is rejected
outright rather than stored as a half-gift — there would be nobody to send the claim link to.
Bounds enforced server-side: recipient name ≤ 100 characters, message ≤ 500 characters.
_rc_gift_start must be YYYY-MM-DD and a real calendar date.
A gift is only captured if the selling plan itself is marked giftable by the merchant. A cart
carrying the gift properties against a non-giftable plan produces an ordinary subscription, silently
and by design — check giftable on GET /plans.
Configuration you can already read
Every Curobi config metafield is declared access.storefront = "public_read", so the Shopify
Storefront API returns them to a headless front end today, with no Curobi credential involved.
| Metafield | Contains |
|---|---|
product.metafields.app.bundle_plan | The bundle’s selling-plan id |
product.metafields.app.bundle_contents | This period’s locked curated box |
product.metafields.app.bundle_menu | This period’s build-a-box menu — variant GIDs, titles, images, prices |
product.metafields.app.mix_match | Tier config: groupId, sellingPlanId, mode, tiers, min, max |
shop.metafields.app.gift_plans | The shop’s giftable selling-plan ids |
A curobi namespace exists as a legacy fallback for each; read app first and fall back only if
you’re supporting an older install.
So a headless build already has, with zero integration work: which products are subscribable, on what plans, at what cadence and discount, what’s in this period’s box, what the Mix & Match tiers are, and which plans accept gifts.
What still needs the Admin API
Three gaps, and the honest workaround for each.
The Mix & Match pickable catalog. Served through the Shopify App Proxy today, which a headless
front end can’t call — there’s no /apps/curobi path, and cross-origin to .myshopify.com fails
CORS. It’s proxied rather than inlined because a large catalog exceeds the metafield size ceiling,
so another metafield can’t fix it. Read it from
GET /mix-match/{id} server-side for now.
The gift disclosure. Same shape, and it matters more than its size suggests: the disclosure exists so a shopper ticking “This is a gift” learns before add-to-cart whether the buyer keeps being charged. If you can’t fetch it, render the conservative wording — that a recurring gift continues billing the buyer until cancelled. Every failure path in our own implementation falls back that way. Rendering nothing on a fetch failure is the one behaviour to avoid.
The customer portal. Curobi’s portal renders inside Shopify-hosted customer accounts, so there is no way to embed subscription management in your own front end today. Send subscribers to their Shopify customer account page instead — a headless store already using the Customer Account API for login can link them straight there, and the portal works normally once they arrive. Don’t build a bespoke management UI against the portal in the meantime: there is no public endpoint behind it for you to call.
Two limits that will not be lifted
Contracts created by another app are invisible. Curobi holds Shopify’s
read_own_subscription_contracts / write_own_subscription_contracts scopes. It can’t read, manage
or report on a subscription another app created, even on a store where Curobi is installed.
Migration is via CSV import, which creates real Curobi-owned contracts. There
is no read-only overlay mode, and this is a Shopify scope boundary rather than a Curobi decision.
Gift recipients can never have a login. Shopify has no third-party-payer concept — the contract belongs to whoever holds the card, and a contract can’t be transferred between customers. Recipients get a tokenized claim page instead of portal access — and you don’t have to build it. Curobi serves it through the Shopify App Proxy, so the emailed link reaches the recipient whatever your storefront is written in.
The catch for a headless store: it renders as Liquid inside the shop’s online-store theme, not your front end. That needs a published theme to exist, and it will inherit however that theme looks. Keep it presentable — to a recipient who has never heard of Curobi, a page in the merchant’s own branding on the merchant’s own domain is the difference between a gift and a phishing email.
Capability matrix
| Capability | Headless today | With the Admin API |
|---|---|---|
| Read plans, cadence, discounts | ✅ metafields | ✅ |
| Create a subscription at checkout | ✅ sellingPlanId | ✅ |
| Curated box contents / build-a-box menu | ✅ metafields | ✅ |
| Build-a-box pick capture | ✅ cart-line property | ✅ |
| Mix & Match tier config | ✅ metafield | ✅ |
| Mix & Match pickable catalog | ❌ | ✅ server-side |
| Mix & Match live price preview | ⚠️ port the math, or none | ❌ |
| Mix & Match checkout discount | ✅ function runs server-side | ✅ |
| Gift capture at checkout | ✅ cart-line properties | ✅ |
| Gift disclosure before add-to-cart | ❌ use conservative copy | ✅ server-side |
| Customer portal | ❌ link to Shopify customer accounts | ❌ |
| Gift recipient claim page | ✅ App Proxy, needs a published theme | ✅ |
Checklist before you ship
Cart lines carry sellingPlanId for subscriptions, and omit it for one-time purchases
Build-a-box lines carry _rc_box_pick with GIDs, plus a readable Your picks
Every line of one Mix & Match selection carries the same _rc_mm_group
Gift lines carry _rc_gift and _rc_gift_email, plus the two visible gift properties
The gift disclosure renders conservative copy when it can’t be fetched
You placed a real test order
And confirmed the box selection, gift or bundle actually appears in the Curobi admin. Nothing above throws when it’s wrong, so this step is the test.