Current state
One Cart Across Two Storefronts
What shapes does BigCommerce actually permit?
A reusable analysis of the shared-cart problem on BigCommerce multi-storefront: what the platform forces, what it allows, and which options are worth costing.
No merchant, agency, or engagement detail appears here. Every platform claim was pulled from BigCommerce's published API specifications, developer documentation, live storefront schema, or support documentation — or observed directly in a development sandbox running two live storefronts, one Stencil and one Catalyst, on a single store.
Verified August 6, 2026 against repository HEAD. Re-pull before quoting.
The problem
Two customer-facing brand storefronts. A shopper adds an item on one, crosses to the other, adds a second item, and should see one cart and check out once.
This is not a setting. It is an architecture, and the platform permits fewer shapes than most proposals assume.
Two constraints that collapse the design space
A cart belongs to exactly one store. Line items reference store-scoped product IDs. No cart can span two store instances. If two brands run as two stores today, consolidating them into one store is not a preference — it is the precondition for any shared cart at all.
A cart's channel is fixed at creation and cannot be changed. PUT /v3/carts/{cartId} accepts one field, customer_id. The operation is named "Update Customer ID." There is no call that moves a cart between channels.
Everything else follows. One store, one channel owns the cart, both storefronts write into it. That is not one architecture among several — it is the only shape available.
What remains genuinely open is narrower: which channel owns the cart, how writes reach it, when the merge happens, and what the storefronts are built with.
The six options
| Approach | Feasible | |
|---|---|---|
| 1 | Native themes plus a server-side cart service writing to a dedicated checkout channel | Yes |
| 2 | Headless storefronts sharing one cart channel | Yes, with a pricing limitation |
| 3 | Keep native channel carts; merge them into one cart at checkout | Yes — commonly overlooked |
| 4 | Two brand domains on a single channel | No |
| 5 | Skip consolidation; mirror both catalogs into a third checkout-only store | Technically yes, operationally poor |
| 6 | Do not share the cart; sequential checkout, two orders | Yes, and worth ruling out explicitly rather than silently |
Why option 4 fails
One site per channel. GET /channels/{channel_id}/site returns a single site and is documented as an alias of GET /sites?channel_id=. The Site schema's own description says a channel may have "zero or more" sites, which contradicts the singular endpoint. Treat as one-to-one until tested.
Why option 5 is technically possible
Designate a store that holds the union catalog and serves only cart, checkout, and orders. The brand stores keep their storefronts. This avoids merging two production stores, which matters when a multi-storefront licence is not in place.
It costs catalog, inventory, and price synchronisation across three stores, and orders land where the products are not managed. Name it, reject it on the record, and move on — but do not omit it, because it is the only path that does not block on licensing.
The finding that usually decides between options 1 and 2
The REST Cart API supports a per-line price override. The GraphQL storefront cart does not.
On POST /v3/carts, a line item's list_price is documented in the API specification as "Optional price override," and name as "Optionally, provide a value to override the product name."
The GraphQL storefront CartLineItemInput accepts quantity, productEntityId, variantEntityId, sku, and selectedOptions. Nothing else. CreateCartInput carries no channel either — the channel comes from the storefront token.
The consequence for two brands selling overlapping catalogue at different prices: a server-side REST cart service can charge each brand's own price and show each brand's own product name inside one shared cart. A headless storefront using the native GraphQL cart cannot, unless it also adds a server-side REST service — at which point it has built the same component the native-theme option needs.
This routinely inverts the intuition that headless is the more capable path. On per-brand pricing it is the less capable one.
Tested, and it has a condition. By default an overridden list_price is excluded from product-level promotions — a 20% product promotion discounted an untouched line from 9.99 to 7.99, while lines overridden to 3.33 and to 20.00 both took zero. The direction of the override is irrelevant; its presence disqualifies the line.
Order-level promotions are unaffected, so cart-value discounts and free-shipping thresholds keep working.
/v3/promotions/settings exposes promotions_apply_on_products_with_custom_product_price, defaulting to false. Enabling it restores product promotions and calculates them against the overridden price — the 3.33 line took 0.67, twenty per cent of 3.33 — with product and order promotions stacking.
Two caveats. The setting is store-wide with no channel scoping, so it changes behaviour for every price-overridden line in the store, including manual orders and any other integration setting a custom price. And the endpoint is a full replace: a PUT carrying one field returns 422 listing the other three as required.
Demonstrated on a live storefront. A cart created through the Carts API on the checkout channel, holding one product from each of two disjoint brand catalogues with a list_price and name override on each, was opened via its cart_url in a browser. Both lines rendered at their overridden prices and names, with correct subtotal, discount, tax and total, and the hosted checkout loaded with both items. One of the two products is not assigned to that channel and does not appear in its catalogue; it rendered, priced and checked out regardless.
Option 3 deserves costing more often than it gets it
Write-through interception is the common proposal: intercept every cart write in the theme and send it to a service that writes to the shared channel. It works, and it carries a specific tax.
Because interception is client-side, it only catches clicks inside the site. A shopper landing directly on /cart.php?action=add&product_id=N from an email or advertisement has the item added server-side before any script runs. The usual remedy is a page-load "drain" that moves stray local-cart items to the shared cart. The native mini-cart and local coupon logic must also be disabled, because both operate on a cart that is intentionally empty.
Merge-at-checkout removes all of it. Each brand keeps its native channel cart. Nothing is intercepted. At cart view or checkout, a service reads both channel carts, creates one cart on the checkout channel with the union of line items — each pinned with list_price — and redirects.
What disappears: add-to-cart interception, quick-add link interception, the disabled mini-cart, the disabled coupon logic, the cart-identifier cookie, cross-domain link decoration, and the drain.
The failure mode also inverts, and this is the part that matters most. A write-through design has no safe fallback by construction, because falling back to the native path would silently write to the wrong cart. So the design fails loudly — which means one small service being unavailable stops add-to-cart on both brands simultaneously. Under merge-at-checkout, a service outage costs the cross-brand merge, not the ability to sell.
Costs of merge-at-checkout: the cart badge needs an aggregated count, the shopper does not see one cart until they act, and the cross-domain guest identity problem persists — though it now degrades the merge rather than the cart.
Neither is universally right. The point is that the comparison is usually never run.
Webhooks: wrong transport, missing safety net
Channel-scoped cart webhooks exist and are precise:
store/channel/{channel_id}/cart/lineItem/created,/updated,/deletedstore/channel/{channel_id}/cart/created,/updated,/deleted,/couponApplied,/abandoned,/converted
Subscribing per brand channel means you hear each brand's cart activity and never your own writes to the checkout channel. Payloads are thin — cart_id and cart_item_id — but GET /v3/carts/{cartId} returns the full cart including channel_id and customer_id.
They cannot be the add-to-cart path. They are post-hoc by definition: the item is already in the brand cart before you hear about it. The first delivery retry is 60 seconds, escalating to 24 hours, with the subscription deactivated after 48 hours of failure. Blocklisting is calculated per destination domain, not per subscription — the documentation states explicitly that two hooks on the same domain affect each other's failures and retries — so an unrelated integration can stall cart merges. No ordering guarantee is documented.
They are the right tool for three other jobs, and they are usually missing from these designs:
Leak detection. A page-load drain runs only when the shopper next loads a page on that brand. A shopper who follows an email link and then leaves never triggers it. The webhook fires server-side regardless. Keep the drain as the fast path; add the webhook as the backstop.
Integrity alarms. Feeds, external buy buttons, and reorder links create carts on channels that are supposed to stay empty. A channel-scoped subscription is the alarm that says a cart exists where none should.
Order correlation. store/cart/converted returns both cart and order identifiers — the clean join for brand attribution and downstream routing, better than inferring it.
Attribution: what survives conversion, and the join nobody costs
Three tested facts decide how per-line brand attribution can work at all.
Cart metafields do not become order metafields. Tested across all five permission sets, with the order's metafield collection checked at 0, 5 and 30 seconds. Empty every time.
But the cart survives conversion. Its metafields stay readable through GET /v3/carts/{cartId}/metafields after the order exists. So the working sequence is: write attribution to the cart, correlate on store/cart/converted, read it back, and write it onto the order — necessary because carts are valid for only 30 days from last modification. A shipping BigCommerce subscriptions app uses this same shape, which is useful corroboration.
The join is the unsolved part. Tested with the same SKU twice in one cart at two prices — the overlapping-catalogue case that motivates per-line pricing:
| Identifier | Cart | Order | Joins? |
|---|---|---|---|
| Line item ID | UUID | integer | No |
product_id | same on both lines | same on both lines | Cannot discriminate |
name override | per brand | survives | Yes |
base_price | per brand | survives | Yes |
A line-to-brand map is only useful if it can be joined to the order, and no cart line identifier survives. Product identity fails precisely where both brands sell the same SKU. Price is a fragile key — two brands can coincidentally match, and promotions move it.
That leaves the per-line name override as the only reliable discriminator that reaches fulfilment, and it is customer-visible on the order and the packing slip. Any design proposing an order-level map or an external mapping service has to name its join key first, or confirm the catalogues share no SKUs.
Order line sequence mirrored cart sequence in testing, which would permit a positional map, but ordering is not documented as guaranteed.
The constraint that decides whether a shared cart is acceptable at all
Promotions are channel-scoped, and the cart's channel is the one that counts.
POST /v3/promotions accepts a channels array, and the scoping is enforced on both sides. Tested: an automatic promotion scoped to one channel applied to that channel's cart and not to another's. A coupon code from a channel-scoped promotion was refused outright on a different channel — 400, "This promotion cannot be used in this channel."
Because the shared cart is created on the checkout channel, and a cart's channel cannot be changed, every promotion and coupon scoped to a brand channel stops applying to that brand's shoppers.
The only remedy is to re-scope all of them to the checkout channel. Channel is the finest available scope, so once re-scoped, each brand's promotions and codes apply to the other brand's products too. Brand-exclusive promotional pricing and brand-exclusive coupon codes are not available under a shared cart.
For merchants whose demand depends on brand-specific promotional calendars, this may matter more than anything else on this page. It is also cheap to overlook, because nothing fails loudly: promotions simply stop discounting, and the first signal is a conversion drop.
Migration implication: inventory and re-scope every live promotion before cutover, not after.
Consequences of one shared checkout channel
These follow from the architecture, not from any particular implementation of it.
Order attribution collapses to the checkout channel. Native per-channel reporting can no longer say which brand drove the sale. Recovering it requires line-item or order metadata and an analytics layer keyed on origin.
Order history, returns, and transactional email live on the checkout channel. The brand sites' account areas will not show these orders. That channel's account pages and email templates must be designed to serve both brands.
Payments, tax, and shipping are configured and settled on the checkout channel for both brands.
Guest cross-domain persistence is link-mediated, not absolute, whenever the brands sit on separate apex domains. The cart follows a guest only through decorated links; a typed URL, bookmark, or organic search arrives without it. Safari caps script-set cookies at roughly seven days. Signed-in shoppers can be recovered from a server-side customer-to-cart mapping. Absolute guest persistence requires a shared parent domain, which is a business and DNS decision rather than an architectural one.
Published ceilings, and why they change the answer
The limits differ by storefront technology, which is easy to miss and occasionally decides the whole question.
| Products | SKUs | Storefronts | |
|---|---|---|---|
| Stencil multi-storefront | 250,000 | 1,000,000 | up to 8, plan-dependent |
| Catalyst | 500,000 | 2,000,000 | 15 |
Per product: 600 SKUs. Per option: 250 values.
If a combined catalogue falls between the two rows, the native-theme option is unavailable and headless is mandatory. Run the census before choosing an approach, not after.
Two behaviours worth checking that most designs skip
Cart size limits change when you route through the API. Guest carts created in the storefront hold 50 items. Carts created via the Carts API hold 600, for guests and customers alike. Funnelling writes through a service raises the guest ceiling twelvefold.
A customer can hold several live carts on one channel, and you cannot ask which is current. Tested: a customer with a saved cart signed in while a service-built cart was active. The active cart survived and bound to them; the saved cart was neither merged nor deleted. Persistent Cart did not intervene. GET /v3/carts?customer_id= returns 404 — there is no endpoint that lists a customer's carts. A cart service therefore has to store cart identifiers itself, which makes that store a system of record rather than a cache. Expect orphaned duplicates in abandoned-cart reporting.
Attaching a customer to an existing guest cart does not drop promotions, despite the specification. PUT /v3/carts/{cartId} warns that changing customer_id removes promotions and shipping calculations. Tested against the fullest cart the warning applies to — a coupon code, an automatic cart promotion, a consignment with address, and a selected shipping option. Nothing dropped; every figure was identical before and after. Treat the warning as stale, but re-verify, because it is cheap to check and expensive to be wrong about.
Channel product assignment is not a sales boundary. Neither the Carts API nor the storefront's own /remote/v1/cart/add refuses a product that is unassigned to the channel. Assignment governs catalogue and search display. Helpful here — the checkout channel needs no catalogue duplication — and worth knowing as an integrity risk elsewhere.
Customer channel scoping fails two different ways, and neither is graceful. A customer whose channel_ids excludes the checkout channel is silently dropped to a guest by the Cart API (201, customer_id: 0), and rejected at the storefront login page with "Your email address or password is incorrect." The password is correct. POST /v3/customers/validate-credentials returns is_valid: false both for a channel rejection and for a genuinely wrong password, so the storefront has no signal to render a better message — this is platform behaviour, not a theme defect. The shopper resets a password that was never wrong, and fails again.
What this analysis does not establish
That any option is right for a given merchant. The constraints are the platform's; the choice depends on catalogue overlap, pricing divergence, integration surface, and how much storefront replacement a business will accept.
Two behaviours remain untested here. Whether a mixed cart completes payment — testing reached the hosted checkout with a correct total but attempted no payment. And the guest cart item ceiling on the storefront, documented at 50 against the Carts API's 600.
One observation is recorded without explanation. The documented add-to-cart deep link /cart.php?action=add&product_id=N added nothing on the storefront tested, for any product, via both real navigation and fetch, while the same products added successfully through /remote/v1/cart/add in the same session. The cause was not established and one storefront proves nothing about the platform. Because the merge-at-checkout argument above rests partly on deep links bypassing client-side interception, verify deep-link behaviour on the actual storefronts before relying on that argument in either direction.
Sources
bigcommerce/api-specs—reference/carts.v3.yml,reference/channels.v3.yml,models/webhooks/store_cart_*.ymlbigcommerce/dev-docs—docs/api-docs/headless/carts.mdx,cart-and-checkout/cart-and-checkout-overview.mdx,cart-and-checkout/add-to-cart-url.mdx,cart-and-checkout/channels-sites-routes.mdx,channels/channel-webhooks.mdx,webhooks/about-webhooks.mdx,webhooks/webhook-events.mdxbigcommerce/docs—docs/storefront/cart-checkout/guide/add-to-cart-url.mdx- Live GraphQL storefront schema, via a Catalyst checkout
- BigCommerce support — Platform Limits, Persistent Cart
- Direct observation in a development sandbox: two live storefronts (Stencil and Catalyst) with disjoint catalogues on one store, driven through the REST Management API and a real browser session. Every object created was removed afterwards and the removal verified