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 |
shop.metafields.app.plan_schedules | Fixed-calendar plans only, keyed by numeric selling-plan id: { "<id>": { "d": charge day, "m": [months], "c": cutoff days, "s": ship day or null } } |
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, which plans accept gifts, and which plans bill on a fixed calendar — with the charge day, months, cutoff and ship day to render an “Order by … · ships …” line.
What the Curobi browser APIs add
The public metafields cover the configuration that fits in Shopify’s storefront data. The remaining interactive pieces use two browser-safe Curobi surfaces, both behind a publishable key and an origin allowlist — never an Admin API key in the browser.
The Mix & Match pickable catalog and live price preview. The
Storefront API returns the paged catalog that cannot fit in a
metafield, plus the server’s own tier calculation for the selection currently in the picker. Use
the returned selling_plan_id and cart_group_id when you turn that selection into cart lines.
The gift disclosure. GET /gifts/terms/{selling_plan_id} on the
Storefront API returns the plan’s finished disclosure sentence, so a
shopper sees the billing terms before adding a gift subscription. If that request fails, keep the
conservative fallback wording: a recurring gift continues billing the buyer until cancelled. Do
not render no disclosure at all.
The customer portal. The Customer API lets a storefront exchange its Shopify Customer Account API token for a short-lived Curobi session, then run the Curobi-owned portal features on its own pages: box picks, rescheduling, in-plan swaps, payment-method update emails, notification preferences, and the cancellation save flow. Shopify’s own Customer Account API remains the right surface for pause, resume, cancel, skip, and address edits.
For a ready-to-fork implementation of the purchase path and portal, see Reference React components.
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 | Shopify Storefront API only | With Curobi browser APIs |
|---|---|---|
| Read plans, cadence, discounts | ✅ metafields | ✅ |
| Fixed-calendar schedule (charge day, months, cutoff, ship day) | ✅ plan_schedules metafield | ✅ schedule on Storefront API GET /plans |
| 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 | ❌ | ✅ Storefront API |
| Mix & Match live price preview | ⚠️ port the math, or none | ✅ Storefront API |
| Mix & Match checkout discount | ✅ function runs server-side | ✅ |
| Gift capture at checkout | ✅ cart-line properties | ✅ |
| Gift disclosure before add-to-cart | ❌ use conservative copy | ✅ Storefront API |
| Customer portal | ❌ Shopify-hosted account pages only | ✅ Customer API + Shopify Customer Account API |
| 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.