Implementation guide: Shopify headless multi-brand checkout on one parent account
- Shopify headless with a parent merchant account means the Storefront API powers your custom frontend while the parent gateway owns the checkout — you skip Shopify Payments entirely.
- The load-bearing decision is where the cart lives: Shopify cart (draft orders) vs. your own cart service. For multi-brand, your own cart service wins because one cart can hold SKUs from multiple Shopify stores.
- Expect 60–80 engineering hours for the first brand, 10–15 for each additional brand. The investment pays back inside 120 days on portfolios above $500k/month.
On this page
Shopify locks merchants into Shopify Payments with an explicit tax — use any other gateway and Shopify charges a 0.5–2.0% extra transaction fee on top of your processor's rate. For a single brand this is annoying. For a multi-brand portfolio it is a five- to six-figure annual penalty. The escape hatch is headless: strip Shopify down to a product and inventory API, run the cart and checkout on your own stack, and settle payments through a parent merchant account that neither knows nor cares that Shopify exists. This guide walks the exact wiring.
1. Why headless for multi-brand specifically
Hosted Shopify tries to make each store an island — its own checkout, its own Shopify Payments account, its own analytics. For portfolio operators this is the opposite of what you want. Headless flips the relationship: each Shopify store becomes a product database, and your custom frontend composes cross-brand carts, shared loyalty, shared upsell logic, and a single checkout that settles through the parent.
The 0.5–2.0% transaction-fee penalty disappears because you never invoke Shopify Payments and you never touch the Shopify checkout. Legally, this is inside Shopify's ToS as long as you use the Storefront API (not Checkout API) and do not call your thing a Shopify Checkout extension.
2. Architecture at a glance
The stack has four layers:
- Shopify stores (one per brand): product catalog, inventory, order writeback.
- Frontend (Next.js / Hydrogen / Remix): renders product pages, cart, checkout UI. Reads from Storefront API. Writes orders to Shopify via Admin API after payment success.
- Cart service (your own, Redis-backed): session-scoped cart holding items from any brand.
- Parent gateway: tokenizes card, runs 3DS, charges with per-brand descriptor, fires webhook to your order-writeback service.
3. Storefront API wiring
Each brand's Shopify admin generates a Storefront API access token with unauthenticated_read_product_listings, unauthenticated_read_product_inventory, and unauthenticated_write_checkouts scopes (the last one is a legacy permission — you only need it if you later switch to Shopify Checkout). Store tokens in your backend, never in the client bundle.
// lib/shopify.ts
const SHOPIFY_STORES = {
brand_a: { domain: 'brand-a.myshopify.com', token: process.env.BRAND_A_STOREFRONT_TOKEN },
brand_b: { domain: 'brand-b.myshopify.com', token: process.env.BRAND_B_STOREFRONT_TOKEN },
brand_c: { domain: 'brand-c.myshopify.com', token: process.env.BRAND_C_STOREFRONT_TOKEN },
};
export async function fetchProduct(brand, handle) {
const { domain, token } = SHOPIFY_STORES[brand];
const res = await fetch(`https://${domain}/api/2024-01/graphql.json`, {
method: 'POST',
headers: { 'X-Shopify-Storefront-Access-Token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ query: PRODUCT_QUERY, variables: { handle } }),
});
return res.json();
}4. Cross-brand cart service
Store the cart in Redis keyed by a signed session cookie. Each line item carries brand, variant_id, quantity, price, image, and sku. When the user checks out, your checkout endpoint sums the cart, fetches the live price from each Shopify store's Storefront API (defensive re-pricing), calculates tax and shipping, and hands the total to the parent gateway.
// POST /api/checkout/init
const cart = await redis.get(`cart:${sessionId}`);
const lines = JSON.parse(cart);
const total = await recomputeTotal(lines); // hits Storefront API per brand
const intent = await parentGateway.createPaymentIntent({
amount: total,
currency: 'USD',
metadata: { session_id: sessionId, brands: lines.map(l => l.brand) },
statement_descriptor_suffix: lines.length === 1 ? BRAND_DESCRIPTORS[lines[0].brand] : 'MULTIBRAND',
});
return { client_secret: intent.client_secret };The statement_descriptor_suffix decision is non-trivial: if the cart is single-brand, use that brand's descriptor. If the cart is multi-brand, use a portfolio descriptor like MF*MULTI with the line-item breakdown in the customer's emailed receipt. Chargeback risk rises with generic descriptors, so keep receipts crisp.
5. Payment element integration
On the frontend checkout page, mount the parent gateway's payment element (Stripe Elements, Adyen Components, or whatever the parent provides) with the client_secret from step 4. Confirm the payment client-side, then on success, POST to /api/checkout/complete with the payment intent ID.
6. Order writeback
The /api/checkout/complete endpoint does three things: verifies the payment intent succeeded via the parent gateway API, writes one order to each affected Shopify store via the Admin API (using orderCreate mutation with financial_status: PAID), and fires the order-confirmation email. This is the most error-prone step — idempotency matters enormously here. Use the payment intent ID as the idempotency key so double-submits do not create duplicate orders.
// POST /api/checkout/complete
const intent = await parentGateway.retrievePaymentIntent(piId);
if (intent.status !== 'succeeded') throw new Error('Payment not complete');
const existing = await db.orders.findOne({ payment_intent_id: piId });
if (existing) return existing; // idempotency
const orders = [];
for (const brandLines of groupByBrand(intent.metadata.lines)) {
const order = await shopifyAdmin[brandLines.brand].orderCreate({
line_items: brandLines.items,
financial_status: 'PAID',
transactions: [{ kind: 'sale', gateway: 'manual', amount: brandLines.subtotal }],
});
orders.push(order);
}
await db.orders.insert({ payment_intent_id: piId, shopify_orders: orders });
return orders;7. Refund routing
Refunds are the hardest part of multi-brand headless. A customer emails you asking for a refund on their multi-brand cart — which brand gets the refund? The pattern: refunds are issued per-line-item, not per-payment-intent. Your admin tool lets the operator select the lines to refund, computes the refund amount, calls parentGateway.refund(piId, amount), and also calls the corresponding Shopify store's orderRefund mutation so Shopify inventory and analytics stay consistent.
See refund routing across sub-brands for the full refund flow.
8. Inventory sync under contention
Two customers buy the last unit of brand_a/sku_X at the same moment. Who wins? The answer depends on when you reserve inventory. Reserve at cart-add (safe but causes abandonment to lock stock) or reserve at payment-intent creation (better UX but requires compensating rollback if payment fails). We recommend the second pattern with a 15-minute TTL on the reservation.
9. Apple Pay on headless
Each brand's frontend domain needs an Apple Pay domain association file served from /.well-known/apple-developer-merchantid-domain-association. In Next.js, place the file in public/.well-known/ and Next will serve it. Trigger verification from the parent gateway dashboard per domain.
10. Gotchas
- Shopify order count limits: Admin API caps orderCreate at 40 per minute per store. If you run a Black Friday spike, queue writebacks and acknowledge payment separately from inventory decrement.
- Tax calculation: Do not rely on Shopify's tax engine for the checkout total — it runs inside Shopify checkout, which you are not using. Use Avalara, TaxJar, or Stripe Tax called from your checkout service.
- Shopify discount codes: Discount codes configured in Shopify admin do not apply automatically to your custom checkout. Rebuild discount logic in your cart service or duplicate the discount model.
- Webhooks: Shopify webhooks (orders/create, orders/paid) still fire when you orderCreate via Admin API. If downstream apps (Klaviyo, fulfillment) listen to those, you get the integrations for free.
- SEO: Shopify product pages at
brand-a.myshopify.com/products/*stay indexable unless you canonical them to your custom frontend. Always set the canonical URL in product metafields.
11. Cost and ROI
For a 6-brand portfolio doing $1.2M/month total, headless migration costs roughly $30k in engineering and takes 8 weeks. Annual savings from dropping Shopify Payments' extra-gateway tax: $72k–$288k depending on the per-transaction penalty tier. Plus you gain full control over checkout UX, dispute evidence (not locked behind Shopify), and analytics. Payback is 90–150 days.
Ready to architect your headless multi-brand checkout? Submit the 12-question intake and we will return a build plan within 72 hours. Or review how the parent routing actually works before committing.
FAQ
Does this work with Hydrogen or only Next.js?
Will Shopify kick us off for using a non-Shopify gateway?
Do we lose Shopify Shop Pay?
Can we keep some brands on hosted Shopify and only go headless for others?
How do we handle subscriptions?
Keep reading
Implementation guide: recurring billing with dynamic descriptors
Subscription engine that plugs into the headless stack.
Implementation guide: refund routing across sub-brands
Multi-brand cart refund mechanics.
multiflow pricing
Setup fee and volume tiers.
How multiflow routes
Parent-account architecture reference.