Run Valotrix Cart Rewards on a headless storefront
This article is for developers who run Shopify checkout but build their own storefront on top of Shopify's Storefront API - Hydrogen, Next.js Commerce, Astro Storefront, Vue Storefront, or any custom React / Svelte / SolidJS setup. Standard Online Store themes don't need any of this; the Theme App Extension handles everything. Skip ahead if that describes you.
TL;DR - what you build vs what Valotrix Cart Rewards gives you
| Valotrix Cart Rewards already handles | You build (or copy from this article) |
|---|---|
| Rules + plans + UI for configuring campaigns in the admin | Fetching the rules from the public API, evaluating them against your cart |
| The discount Function at checkout (applies the configured percentage to gift lines automatically - no headless code path involved) | Adding gift lines to the cart with the right line attributes |
| Per-customer limits, OOS auto-disable, plan caps, GDPR redaction | Rendering progress bars, popups, toast notifications in your own component tree |
| Outbound webhooks (gift.added, order.converted, …) | Posting gift / choice events back to Valotrix Cart Rewards for analytics |
| Cart Transform "FREE GIFT" line-title rewrite (Plus-only) | - |
| Plus checkout-extension banners | - |
The crucial piece: the discount Function at checkout doesn't care how a gift line got into the cart. As long as the line carries the _vltrx_campaign_id and _vltrx_rule_id line attributes, the Function discounts it. So a headless storefront's job is to add the right gift lines with the right attributes - Shopify's checkout takes it from there.
Prerequisites
- Scale plan - required for the public API key. Lower plans don't expose a programmatic way to fetch rules.
- A custom Shopify-private app or Hydrogen with Storefront API access for the cart-mutation half.
- A Valotrix Cart Rewards API key - Settings → API & Webhooks → API Keys → Generate.
The five-step flow
- Fetch active rules from
GET /api/v1/rules. - Evaluate which rules match the current cart, customer, and market.
- Mutate the cart to add (or remove) gift lines, stamping the
_vltrx_*line attributes. - Render UI in your own component tree using cart state + matched-rule data.
- Report events to
POST /api/v1/eventsso analytics stay accurate.
The rest of this article walks each step with code.
Step 1 - Fetch active rules
GET https://vltrx-rewards-api.valotrix.com/api/v1/rules
Authorization: Bearer <your-scale-api-key>
Response shape:
{
"rules": [
{
"id": "ruleabc",
"campaignId": "clxabc123",
"name": "Spend $50 - free tote",
"enabled": true,
"conditionTree": { "type": "cart.subtotal_gte", "value": 5000 },
"giftConfig": {
"type": "fixed",
"gifts": [
{ "variantId": "gid://shopify/ProductVariant/12345", "quantity": 1 }
],
"fallbackOnOOS": false
},
"options": {
"actionType": "auto_add",
"discountMethod": "automatic",
"discountType": "percentage",
"discountValue": 100,
"discountTitle": "FREE GIFT"
}
}
],
"experiments": [],
"shopSettings": { /* engine + checkout + data flags */ },
"widgets": { /* widget config */ }
}
This endpoint supports HTTP caching via ETag / If-None-Match (returns 304 Not Modified when nothing has changed). In Hydrogen / Next.js, cache the response in your edge layer with a 60-second TTL - that's the same TTL the standard storefront uses.
See Public REST API → Rules for the full payload shape.
Step 2 - Evaluate rules against the current cart
This is the part with no Valotrix Cart Rewards package today. You have two paths:
Path A - full engine parity. Implement the condition evaluator following Reference → Condition types. The condition tree supports AND / OR / NOT over 17 leaf condition types. This is what the standard storefront does. The downside is drift: every engine change in Valotrix Cart Rewards needs a corresponding update on your side.
Path B - define your campaigns directly in headless code. Skip the rule-evaluation engine entirely. In your headless storefront's cart logic, hard-code the conditions you care about ("cart subtotal ≥ $50 → add tote") and use /api/v1/rules only as a sanity check or to fetch the gift variant IDs. You still get the discount at checkout (because gift lines carry the _vltrx_* attributes), and you still get analytics (because you POST events). What you lose is the ability for non-developers to change campaign logic in the admin and see it reflected on the headless storefront - every config change requires a code deploy.
For most stores, Path B is the pragmatic choice today. Use the admin to track campaign metadata (gift variant IDs, analytics, plan caps, OOS handling); use code to evaluate the conditions. When the headless engine package ships, you'll be able to drop in Path A without rewriting the rest.
The minimal evaluator you need:
// pseudo-code; adapt to your stack
function shouldAddGift(cart: Cart, rule: Rule): boolean {
// Example: cart subtotal threshold
if (rule.conditionTree.type === "cart.subtotal_gte") {
const subtotalCents = Math.round(cart.cost.subtotalAmount.amount * 100);
return subtotalCents >= rule.conditionTree.value;
}
// Add cases for the conditions you actually use
return false;
}
Step 3 - Mutate the cart with the right line attributes
This is the most important step. Every gift line MUST carry these line attributes:
| Attribute | Required | Value |
|---|---|---|
_vltrx_campaign_id | yes | the rule's campaignId |
_vltrx_rule_id | yes | the rule's id |
_vltrx_auto | recommended | "true" (lets Valotrix Cart Rewards recognize it as an auto-add) |
_vltrx_title | optional | merchant-readable title (used by the Cart Transform Plus rewrite) |
_vltrx_lock_quantity | optional | "true" if you want the customer locked at quantity 1 |
_vltrx_discount_method | recommended | "automatic" (or whichever the rule uses) |
The discount Function at checkout looks for _vltrx_campaign_id to identify gift lines. Without it, the discount won't apply and the customer will be charged the full price.
Hydrogen - cartLinesAdd
// In your loader / action:
import { cartLinesAddDefault } from "@shopify/hydrogen";
const result = await cart.addLines([
{
merchandiseId: rule.giftConfig.gifts[0].variantId,
quantity: 1,
attributes: [
{ key: "_vltrx_campaign_id", value: rule.campaignId },
{ key: "_vltrx_rule_id", value: rule.id },
{ key: "_vltrx_auto", value: "true" },
{ key: "_vltrx_title", value: rule.options.discountTitle ?? "Free Gift" },
{ key: "_vltrx_discount_method", value: "automatic" },
],
},
]);
Next.js Commerce / Vanilla Storefront API
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
lines(first: 50) {
edges { node { id merchandise { ... on ProductVariant { id title } } attributes { key value } } }
}
}
userErrors { field message }
}
}
Variables:
{
"cartId": "gid://shopify/Cart/abc123",
"lines": [
{
"merchandiseId": "gid://shopify/ProductVariant/12345",
"quantity": 1,
"attributes": [
{ "key": "_vltrx_campaign_id", "value": "clxabc123" },
{ "key": "_vltrx_rule_id", "value": "ruleabc" },
{ "key": "_vltrx_auto", "value": "true" },
{ "key": "_vltrx_title", "value": "Free Gift" },
{ "key": "_vltrx_discount_method", "value": "automatic" }
]
}
]
}
Removing a gift when conditions stop matching
When the cart drops below the threshold (or the customer removes the qualifier), remove the gift line via cartLinesRemove. Identify the line to remove by checking its attributes:
const giftLineIds = cart.lines
.filter((line) =>
line.attributes.some((a) => a.key === "_vltrx_campaign_id")
)
.map((line) => line.id);
if (giftLineIds.length > 0) {
await cart.removeLines(giftLineIds);
}
If you skip the removal, the gift stays in the cart but the discount Function only applies when conditions are still met at checkout. Removing eagerly is the right UX in most cases.
Step 4 - Render the UI in your design system
Valotrix Cart Rewards's standard widgets (toast, popup, progress bar, gift-choice modal, reminder banner) live in the storefront script that doesn't load on a headless storefront. You build your own equivalents.
The data you need is on the cart + rule:
- Progress bar:
(cart.cost.subtotalAmount.amount * 100 / threshold) * 100percent. - "Spend $X more" text:
threshold - currentSubtotalCentsformatted in the customer's currency. - Gift-choice modal: if
rule.options.actionType === "customer_choice", render your own modal with the rule'sgiftConfig.gifts[]as options. On selection, callcartLinesAddwith the chosen gift'svariantId(instead of auto-adding). - "Free gift unlocked" toast: fire when your local "is gift line in cart" boolean transitions from false to true.
Match your storefront's visual identity. Valotrix Cart Rewards doesn't gate any of this - it's purely your component tree consuming cart state.
Step 5 - Report events back to Valotrix Cart Rewards
For analytics to stay accurate, post events whenever you add or remove a gift, and whenever a customer interacts with a choice popup:
POST https://vltrx-rewards-api.valotrix.com/api/v1/events
Authorization: Bearer <your-scale-api-key>
Content-Type: application/json
{
"events": [
{
"type": "gift_added",
"campaignId": "clxabc123",
"ruleId": "ruleabc",
"variantId": "gid://shopify/ProductVariant/12345",
"cartToken": "Z2NwLXVzLW1pZ..."
}
]
}
Event types that match the standard storefront:
gift_added- when you mutate a gift line into the cartgift_removed- when you mutate one outchoice_shown- when your custom gift-choice modal openschoice_selected- when the customer picks an option
Without these events, the Analytics dashboard shows zero promotion orders for headless traffic, and Shopify Flow's Promotion gift added trigger never fires for your store. The order.converted webhook still works because Shopify's orders/create webhook fires regardless of how the cart was built.
cartToken is optional - Shopify gives every cart a token; passing it lets Valotrix Cart Rewards tie session-level analytics to the eventual order. Omit if your privacy policy doesn't allow it (turn off Track cart sessions in Settings → Data and the standard storefront also strips it).
Hydrogen-specific tips
- Where the loader runs: Hydrogen's
root.tsxloader runs on every request. Fetch/api/v1/rulesthere (withcache: 'no-store'for development, with caching headers for production) and pass the rules throughloaderDatato your route components. Don't fetch from auseEffecton the client - it adds a render flash. - Cart attributes vs. line attributes: Hydrogen's
cart.cartAttributesUpdateis for cart-level attributes (e.g. delivery instructions). Gift lines need line-level attributes viacart.addLines({ attributes: [...] }). Don't confuse them. - Cloudflare Workers compatibility: the Workers runtime is V8, not Node. Standard
fetch()works, but anything reading fromnode:cryptodoesn't. Stick to Web Crypto APIs if you wire up signature verification (only relevant for webhook receivers, not for API calls to Valotrix Cart Rewards). - Customer-choice popups need session state: when a customer picks a gift in your modal, you need to remember it across page navigations. Hydrogen's session helpers (
session.set('vltrxChoice', ...)) work, or persist on the cart itself via cart-level attributes.
What still works automatically
Even though your headless storefront is doing the cart-side lifting, several Valotrix Cart Rewards features stay fully automatic - you don't write code for any of these:
- The discount Function at checkout. Targets every line carrying
_vltrx_campaign_id, applies the rule's configured discount. Whether the line was added by the standard storefront or your headless code, the Function doesn't care. - Cart Transform "FREE GIFT" line-title rewrite (Plus shops). Operates server-side on Shopify's checkout - your line shows as "FREE GIFT" automatically.
- Plus checkout-extension banners. Six checkout-targeted extension surfaces render the gift confirmation banner on Plus stores. They run inside Shopify Checkout, not your storefront, so they ship regardless of your front-end stack.
- Per-customer limits. Enforced server-side by querying redemption counts at evaluation time. Use
GET /api/v1/redemptions?customerId=…from your storefront's evaluator if you want client-side awareness. - Outbound webhooks. Fire on every relevant event -
gift.added(when you POST it),order.converted(when Shopify completes the order),campaign.activated/campaign.deactivated(when toggled in admin). - Plan caps + OOS auto-disable + GDPR redaction. All admin-side. Nothing for you to wire.
What's NOT supported in headless today
- Click-to-select cart-row inspector. It walks the DOM in the standard storefront to detect cart-row markup. Headless storefronts render their own, so the inspector doesn't apply.
- Theme app blocks (threshold bar, PDP gift preview, urgency banner, etc.) ship as Liquid + JS for Online Store themes. Headless storefronts don't load them - you re-implement the equivalents in your component tree.
?vltrx_debug=trueURL flag for live-cart engine tracing. The flag works on the standard storefront only because the storefront script reads URL params. Your headless storefront would have to implement its own debug log against your evaluator output.- Preview URL (signed 24-hour token from the campaign edit page). The signed token is verified at the Shopify App Proxy, which only works on
*.myshopify.comURLs. For headless, test with a separate dev API key + the simulator instead.
Future: the engine package
Path A above (full rule-tree evaluation in your headless code) is real engineering work today because the engine isn't yet published as an npm package. The kernel at app/engine/ is already isomorphic by design (no Prisma, no Node-only code), and the plan is to publish it as @valotrix/engine once we have enough headless prospects to justify the maintenance overhead. Until then, Path B is the recommended approach.
If you'd like to be on the early-access list when the package ships, email valentin@valotrix.com with a one-line "running Valotrix Cart Rewards on Hydrogen / Next.js / etc." note. We're tracking demand directly.