Support
This is the app's own help documentation, published in full - the same answers a merchant reads inside the product, including the ones about its limits. Anything it cannot answer: [email protected].
Start here
The first ten minutes, in order. The two switches come first because nothing else in the product does anything until they are on.
Getting started
cbsplit does two separate jobs: it splits traffic between versions of a page to find out which sells better, and it shows one-click offers after checkout. You can use either without the other. Both need one switch turned on in Shopify first.
It answers two questions with real money rather than opinion: which version of a page sells better, and what else a buyer will take once they have already paid. Everything else here is machinery for those two.
Split tests need a small piece of code running on your storefront. Without it no visitor is assigned a version, no visit is recorded, and no order is attributed. Nothing errors - it simply does nothing, which looks identical to a test nobody has visited.
You should see: Visit a targeted page in a private window; the test records a visit within a minute.
Theme-specific. Duplicating a theme or installing a new one starts it off again, so a redesign can quietly stop every running test. Also check you are on your LIVE theme - running the Shopify CLI creates its own development host theme, which appears in the same list and does nothing for real visitors.
Upsells need Shopify to hand the buyer to us after payment. Without this, funnels never render however well they are built - buyers go straight to the thank-you page.
You should see: A test order matching a funnel shows the offer between payment and the thank-you page.
Shopify allows exactly ONE post-purchase app per store. Another app in that slot blocks us completely, and the last one selected wins.
Shopify deliberately keeps both under the merchant's control. We can hand you a link that opens the theme editor with the embed already switched on, but you still press Save; the post-purchase setting has to be chosen in checkout settings by hand. An app that could silently modify your live theme and your checkout is one you should not install.
Only for what you use. Split tests and pre-purchase offers need the theme embed; post-purchase upsells need the checkout page. A store running upsells and nothing else can leave the embed off entirely.
Five steps, and it ticks each one from evidence rather than from your word for it. Turning the storefront switch on does not tick that step; the first storefront visit our script actually serves does. Same for the order step, which waits for a real order to arrive and be ingested.
This is why a step can look undone after you have done it: nothing has exercised it yet. It is also why the guide is worth trusting, because a green tick means something was measured on your store rather than assumed. Three conditions block completion even with every step ticked, and each says so in words: checkout tracking that has never reported, an upsell page that has never checked in, and a storefront switch in your theme (the cbsplit app embed, or its Run split tests box) that is off while a split test, pre-purchase offer or free shipping bar it runs is live.
Before you install
The five questions merchants ask about any testing or upsell app, answered for this one specifically.
One small script loads through Shopify's theme app embed system. On a page under test it makes a single call to assign the visitor a version. If you run a pre-purchase offer it also watches for a place to show it - an order bump can appear in the cart drawer, and a drawer opens anywhere - so on those pages it reads your cart once and asks which offer applies. On every other page it does nothing further. There is no tag manager, no third-party beacon farm, and no render-blocking overlay.
Honesty about the one real cost: variants applied in the browser take effect just after the page renders, so a heavily restructured variant can briefly show the original first. Small copy, image and price changes are imperceptible in practice, and the what-it-refuses-to-do page states this limit plainly.
No. The storefront piece is a standard app embed you switch on in the theme editor, and offers are built in a wizard. Nothing edits your theme's code - Shopify does not even allow apps to write theme files, which is a guarantee, not a promise.
The offer appears on Shopify's own post-purchase page, between payment and the thank-you page. If the buyer accepts, Shopify charges their original payment method for the added item - no re-entering card details, and the app never sees them. Decline or close, and the order stays exactly as placed.
Every charge is signed server-side from the funnel the merchant defined. The page in the browser cannot invent a price, a product, or a discount - forged requests are refused.
Any Online Store 2.0 theme takes the app embed, and the post-purchase page is Shopify's native surface on standard plans - none of this is Plus-only. Stores whose landing pages live outside Shopify can run tests through the headless API instead.
Two Shopify rules every upsell app shares: the post-purchase page is skipped for some express payment methods, and it appears only for orders paid in your store's own currency - so set your store currency to where you actually sell.
Every store starts with a 30-day free trial. After that the fee is monthly and set by your previous month's gross sales - free under the lowest threshold, capped at the top tier, never a percentage of revenue. The exact table is on the pricing page and on the approval screen at install; both render from the same definition the billing engine charges with.
Once installed, the Plan page shows which tier you are in, the sales figure that put you there, and every charge made so far. Billing is settled in arrears, so a month is charged on what it actually did rather than on a forecast.
Split testing: how and why
Show some visitors one version of a page and the rest another, then compare what they spent. The point is not the pages - it is replacing an argument about what customers want with a measurement of what they did.
Because most changes do nothing, a meaningful number make things worse, and neither is visible without a comparison. Shipping a redesign and watching revenue is not a test - the season, your ad spend and your stock levels all moved too.
Each visitor is assigned a version on first arrival and keeps it for the whole visit and any return visit. That stickiness is what makes the comparison honest: someone who saw both pages belongs to neither group.
An attribution token rides with the visitor from the page to the cart to the order. When Shopify tells us an order happened, the token says which test and version produced it. Orders that carry no token are left uncounted rather than guessed at.
Yes, but only because it is handled specially. Those build a checkout straight from the product form and never touch the cart, so the token travels as a hidden field on the form as well as on the cart. Before that was added, every accelerated-checkout order attributed to nothing.
Worth knowing if you compare our order counts against Shopify's: a mismatch concentrated in express checkouts points here rather than at your test.
The thing the most people see and the fewest people scroll past. On a product page that is usually the first screen: the headline, the main image, the price presentation, the buy button. Testing a footer link is a valid test of something that does not matter.
Anything Shopify owns outright. Checkout itself is not ours to change, and neither is the post-purchase moment beyond the offer we render. Test everything up to the buy button freely.
One-click upsells: how and why
An offer shown after the card is charged and before the thank-you page. One click accepts it - no card re-entry, no second checkout - because the items are added to the order that was just placed.
Because it cannot cost you the original sale. A cart upsell adds a decision before the money is committed, and every added decision loses some buyers. A declined post-purchase offer leaves the first order exactly as it was.
This is why aggressive offers belong here. The downside of a buyer saying no is zero, which is not true anywhere else in the funnel.
From what was just bought - the variants, products, SKUs and the order subtotal - matched against your funnels' targeting rules. One thing does come from the visit: if the buyer was in a split test and you bound a funnel to their version, that binding is checked first and wins. Bind no funnels to a test and the choice is made from the order alone.
On the original order, as extra line items. There is no second order to reconcile, and Shopify's own totals, fulfilment and returns cover the upsold items as normal.
Yes, unchanged. Post-purchase attaches to Shopify's checkout, which you keep no matter where the page before it lived.
Shopify skips the post-purchase page entirely for some orders, whatever your setup says. The big one is accelerated wallets: Apple Pay, Google Pay, Shop Pay Installments, Klarna, Affirm and Afterpay all check out without one. So do gift cards and anything not paid by credit card, orders paid with more than one method, subscriptions, orders carrying duties or more than one currency, and orders created through the API or from a draft.
Wallets are the reason this looks intermittent rather than broken. If your store takes a lot of Apple Pay, a real share of orders will never be offered anything, and no amount of funnel setup changes that. To test, place a plain single-currency order on a credit card.
Thank-you page offers: one more product, added to the order
A thank-you page offer is a small card on Shopify's Thank you page: a headline, a countdown, one product with its price and saving, and a button, Add to my order. Pressing it adds the product to the order the shopper just placed and sends them to Shopify's page to pay for it. It belongs to a funnel as its last step, after the post-purchase offers.
A post-purchase upsell is one click and charges the card the shopper just used, before the Thank you page. A thank-you page offer comes after that: the product is added to the same order, and the shopper pays the difference on Shopify's own payment page, with any payment method. So it is a second small checkout on the same order, not a one-click charge.
That is Shopify's rule, not ours: nothing on the Thank you page may change the order directly, and an ordinary checkout stores no card an app could charge afterwards. Every app that offers this works the same way.
Add it to a funnel, bind the product, and place the card on your Thank you page in Shopify's checkout editor. Until the store has approved the app's order-editing permission, the card stays off and the funnel page says so.
You should see: Place a test order and reach the Thank you page: the card appears where you put the block, with the product and its price.
One per funnel. The card obeys the funnel's targeting and split tests exactly as the post-purchase offers do, so a shopper in arm B of a funnel test sees arm B's card.
After 30 minutes cbsplit takes the product back off the order and restocks it. If Shopify will not let it be removed, the order is listed in your dashboard's to-do list, because the item may otherwise ship unpaid. Then collect the balance, or remove the item from the order in Shopify. Nothing is counted in the meantime: the offer's revenue is recorded only once the order's balance is back to zero.
As its own row on the funnel's steps and its own segment in the test report's stage bar, labelled Thank-you page. It is never folded into the upsells, because it is a different kind of sale and the point of running both is to see them apart.
Pre-purchase offers: an offer before checkout
A pre-purchase offer is a small add-on offer shown before checkout: beside Add to cart on the product page, or as an order bump above the checkout button in the cart and cart drawer. It is the one surface here that acts before the payment, so it can raise the order itself rather than adding to it afterwards.
A tickbox offer above the checkout button, on the cart page and inside the cart drawer. The shopper ticks it, the product joins the cart, and they check out once. Because it is pre-purchase, the money lands in the original order rather than in a second one.
Both places, not one: a theme's drawer cannot host an app block, so the widget is placed structurally rather than by you dragging it somewhere. That is why it needs no theme editing and why it appears in the drawer at all.
One or more add-ons a shopper can tick beside Add to cart. Ticking only selects: the add-ons ride the theme's own Add to cart, and Buy it now carries them too. They join the cart as lines nested under the product, so Shopify removes them wherever the product is removed: in the cart, at checkout, on the order status page, in emails and in the admin.
You should see: Load a product page the offer applies to and the add-ons appear beside Add to cart.
Unlike an order bump, this one is an app block you place, so it shows only where the block is. Its discount, if any, is the same automatic discount an order bump uses and needs the product it was offered with to be in the cart. One offer shows per placement, so an order bump and a product-page offer can both show on one page.
Pick where it shows and the product it adds, decide who sees it, and activate. A new offer opens on a starting design you can edit, and nothing shows to a shopper until you activate it.
You should see: Load a page it applies to as a shopper: an order bump appears above the checkout button, a product-page offer beside Add to cart once its block is placed.
You name one figure, a percentage or an amount off. That single value is what the widget displays and what checkout charges, so the two cannot disagree. If the charge half cannot be set up, no discount is shown at all rather than a price we cannot honour.
The figure is locked while the offer is live, because Shopify's discount was created from it. Deactivate the offer to change it, then activate again: that rebuilds the charge from the new value.
The same kinds of rule a funnel uses, read against the cart rather than the order. A product-page offer reads its rule against the product being viewed, and its subtotal rule against the cart.
A cart that already holds the offer's own product never sees it. Offering someone what they are already buying is the one case worth ruling out by hand.
The more specific rule wins. An offer targeting a product beats one targeting a collection, which beats a subtotal range, which beats the catch-all. One offer shows at each placement, never two.
A cart holds a snowboard. You have an order bump for that snowboard and another on every cart. The snowboard bump shows, and the every-cart bump takes the carts it did not want.
Where two offers at the same placement have rules of equal reach, the more recently edited one takes all of that traffic and the other shows to nobody. The app says so on the offer that is losing, naming the one taking its traffic, because an offer reading Active with no views and no reason is indistinguishable from broken.
Two shapes. Against no offer, which measures whether having one helps at all, or against a different offer, which measures which of two is better. The wizard asks which after you press the button.
You should see: The report counts shoppers who were shown the offer rather than page visitors, and take rate is accepts over those shoppers.
In a two-offer test the CONTROL's targeting rule decides who is in the experiment, for both arms. Otherwise the arms would reach different shoppers and the test would be measuring reach as well as the offer.
Seven causes, and the first two account for most of it.
Free shipping bars: free shipping over an amount, shown and charged from one value
A free shipping bar tells shoppers how much more to add for free shipping and fills as they add it, on the cart page and in the cart drawer. The amount you set is the amount checkout gives free shipping at: one value, shown and charged, so the bar cannot promise what checkout will not do.
The cart's merchandise subtotal after product discounts and before any code entered at checkout, in your store's currency. Shoppers paying in another currency see the amount converted at your store's own rate, and checkout converts it the same way. An accepted pre-purchase offer counts, because it is in the cart.
A discount code entered AT checkout can lower the subtotal after the bar was read, and a merchant's own discounts that are set not to combine with shipping discounts can take precedence there. Both are Shopify's rules at checkout; the bar reads the cart as it stands.
Activating a bar creates a free shipping discount in your store from the bar's amount. It applies to every parcel of a qualifying order, so a split shipment is still free. If that discount cannot be created, the bar stays off and says why, rather than showing a promise checkout would break.
If your store already gives free shipping over that amount through its shipping rates, nothing changes: the discount takes 100% off a rate that is already zero. The bar is then the messenger for a policy you already had.
Its free shipping is not armed at checkout: the discount was deleted on Shopify, or the amount or rule changed while it was paused and the discount still describes the old bar. The bar serves nobody rather than promising the wrong figure. Activate it again to rebuild the charge from the current amount.
The other direction is worse and is watched too: a bar whose free shipping could not be switched OFF when it was paused says so, because qualifying carts may still ship free with no bar showing. Pausing or activating again retries.
Top of the cart puts it under the cart's heading, in the drawer and on the cart page, wherever your theme has one; where it has none the bar falls back to sitting above the checkout button. Above the checkout button is the proven place on every theme, beside any order bump you have.
It is added by the same app embed the pre-purchase offers use, so it needs no block placed. If the embed is off nothing of ours runs.
The same rule kinds a pre-purchase offer uses, read against the cart: every cart, carts containing a product or a collection, or a cart subtotal range. The free shipping at checkout follows the same rule, so a cart the bar would not show on does not get it either. A cart with nothing to ship sees no bar.
One bar shows per cart. Where two bars have rules of equal reach, the more recently edited one takes every cart and the other says so on its own row.
The OFFER: half your shoppers get the bar and the free shipping at its amount, half get neither. The report's conversion rate, revenue per shopper shown and order value compare the two. That is a test of giving free shipping with a bar announcing it, not of the bar alone.
It cannot run beside a page test: both claim the same orders, so one would report the other's traffic. Stop the page test first, or wait for it to finish. The Shipped free figure is read off each order's own shipping line; on a store with a native free shipping rate the control arm ships free too, and the report will show that.
Setting up a basic split test
Pick what page it applies to, pick how the versions differ, and set the split. The wizard checks your theme can actually serve what you chose before it lets the test run.
A specific product, a collection, a URL pattern, or everything. The target decides which page loads carry the test - a visitor on a page that does not match is never assigned and never counted.
Evenly, unless you have a reason not to. An uneven split is legitimate for limiting exposure to a risky variant, but it makes total revenue meaningless as a comparison, because the version shown to more people wins by construction. Shares always add to 100.
Element changes rewrite parts of the page after it loads, and are what the visual editor produces. Alternate templates serve a different Liquid or JSON template you already made, which is better for large layout differences. A price test changes nothing but the price: you name a percentage and the discounted version is generated for you, with checkout charging the same figure the page shows. Headless hands the decision to your own code.
The visual editor opens your real storefront with an editing layer over it, so you change the real page at real width with your own theme running.
No. Each change is stored as an instruction against a CSS selector and applied only when a visitor is put in that version. Stopping the test removes every change immediately.
The flip side: a change is anchored to an element, so a redesign that removes or renames it leaves the instruction matching nothing and the version quietly renders as the control.
Because Shopify answers a request for a template that does not exist by silently serving the default one. That version would render the control, the test would report no difference, and you would conclude the change did nothing. We check first, and refuse if we cannot check.
When you stop it, or on the rule you set: a date, a number of sales or visits, or reliability. Reliability is the one to prefer - it runs until both versions carry enough traffic to support a conclusion, then judges once.
Avoid stopping the first time a version looks good. Checking repeatedly and stopping on a lead is the single most expensive mistake in testing - see the results section.
You can, and for a typo it is fine. For anything larger, restart it: visitors before and after the edit saw different pages, and their results are pooled into one number that then describes neither.
Advanced split tests: pages you host yourself
Testing a page on your own domain that sells through your Shopify store - a Hydrogen or custom storefront, or a marketing landing page on your own CMS. Fully supported and fully documented, and the one part of the product that expects a developer.
Anyone whose page lives outside Shopify. Everything it needs is documented, but wiring it into your site is your side of the line. Ask about implementation support if you would rather not do it yourself.
No, and you should not. If your site or CMS already splits its own traffic, tell us which version it served and we supply the part it cannot see: what the order was worth after refunds, shipping, tax and cost of goods. A tool watching your frontend only knows a button was clicked.
Two things. A call when the page loads so the visit is counted, and an attribution token on every link into your store so the sale comes back.
Miss the first and every version reports a 0% conversion rate. Miss the second and the test records visits and never a sale. Both look like real results rather than missing wiring.
Three shapes, best first. If you already split your own traffic, just report the version you served. If you have a server or edge worker, ask us before you render. Only if you have neither should you use the drop-in script.
The page cannot render the varying area until the answer arrives. Showing the control first and swapping would make the variant slower on every load, so the test would partly measure network latency instead of your change. We hide the varying area for every version equally: slower for everyone, biased against nobody.
Deciding on your own server avoids the cost entirely, and is always the better integration.
Testing a price
A price test sells the same product at two prices and measures which makes you more, per visitor rather than per sale. It is the one test where the change is not something you design, so there is nothing to build: you name a discount and the arm sells at it.
Choose Test a different price in the New test wizard, pick the product, and say how much off version B sells at. There is no variant to build and nothing to edit: one value decides both the price the page shows and the price checkout charges, so the two cannot drift apart.
You should see: Load the product page as a shopper and version B shows the lower price. Add it to the cart and checkout charges that price.
A discount line on the order, labelled Special price. That is how the lower price is charged: Shopify applies it as a discount rather than by changing the product, so it appears in the order summary the way any discount does, and it stays on the order confirmation and the emailed receipt.
The wording is deliberately neutral. It says nothing about a sale, a promotion or an offer, because a shopper in the control arm is not in one and a label that implied otherwise would follow the discounted buyer around their paperwork.
Because adding one would change what the test measures. A was-price is a second variable: it adds urgency on top of the lower number, and a win would no longer say whether the price or the urgency did it. The arm shows the lower price plainly.
If a struck-through price is the thing you actually want to test, that is a real test and worth running, but it is a page change rather than a price change and belongs in a template or element test.
Yes, and the wizard offers it as Also sell at a percentage off on an ordinary test. It is worth knowing what you are giving up: the arm now differs from the control in two ways at once, so a result tells you the combination won and cannot separate them.
Version B has a new hero image and sells at 10% off, and it wins. You have learned that the pair beats the control. You have not learned whether the image would have won on its own, or whether you are simply buying the sales with the discount.
The wizard says this on the row itself the moment you type a figure, rather than leaving it to be discovered in the result.
Permission to manage discounts, because that is the mechanism the lower price is charged through. It is granted per store when you approve the app, so a store that installed before this existed will be asked to approve it again before the option becomes available.
Until it is granted the option is shown switched off with that reason, rather than hidden. A control that vanishes is indistinguishable from one that was never built, and a merchant cannot fix what they cannot see.
The discount applies to whatever price that buyer's market shows, so a percentage off means the same percentage everywhere rather than a figure converted from your home currency. The variant table in the wizard lists your own market's prices, and says so.
One honest limit. On a page where we cannot read the market-adjusted price, the shopper is shown the price without the discount applied while checkout still charges the discounted one. It resolves in the shopper's favour, and it is the direction we would choose deliberately, but it means a screenshot of a product page is not proof the test is off.
Setting up a basic upsell flow
The simplest useful funnel is one offer shown to everybody. Start there, then add targeting once you know the offer converts.
Create a funnel, add one step, put a product in it, and leave the targeting as global so every order sees it. You can narrow it later without rebuilding anything.
You should see: A test order shows the offer between payment and the thank-you page.
Something cheap relative to what they just bought, obviously related, and needing no thought. The buyer has already decided to trust you and their card is already charged - the offer that works is the one that does not restart the decision.
Nothing. The original order completes exactly as it would have. You can also chain a different step onto the decline, which is where advanced funnels start.
Use the funnel test wizard. It needs no page, no target and no version editing: you pick the funnel you are running now as the control, pick the one to try against it, set the split, and start. Buyers are divided between the two funnels after checkout, and the report measures the funnels rather than any page.
You should see: The report counts orders offered rather than page visitors, and revenue per order decides the winner.
The unit is an order, not a page view, because a funnel is only reached after checkout. Everything else is unchanged: the same sample size rules, the same refusal to call an early winner. If you would rather hold the funnel STILL while testing a page, that is the next answer down.
Yes, and it is worth doing. Bind one funnel to the WHOLE test and every buyer in it gets the same post-purchase experience, so a difference between your page versions cannot be caused by them having met different offers.
Advanced upsell flows: targeting and multi-product steps
Which funnel a buyer gets is decided by a priority hierarchy, evaluated top down. The FIRST level with any matching funnel wins, and everything below it is ignored - so a rule's position matters as much as its contents.
Give the funnel a targeting rule. There are five kinds, and they sit at different levels of a priority hierarchy rather than combining with each other.
Split test this version, then split test as a whole, then product, then collection, then price, then global. Every active funnel is checked at the top level first; if any match there, those are the only funnels considered and the rest are never looked at. The two test levels are separate on purpose: a funnel bound to version B has to beat one bound to the whole test, or setting the more specific rule would lose to the broader one.
A buyer purchases a $200 jacket. You have a product-rule funnel for that jacket, a collection funnel for outerwear, and a global funnel. Only the jacket funnel is eligible - outerwear and global are never reached, even though both also match.
This is why a broad rule never steals traffic from a specific one, and also why a global funnel only ever sees orders that no narrower funnel wanted.
Not today. The order is fixed: split test, then product or variant, then collection, then order subtotal, then global. If a broader rule needs to win over a narrower one, make the narrower funnel's rule match fewer orders, or turn it off - the first rung with a match wins outright.
They are split-tested against each other, using the same weighted sticky assignment as a page test. That is the intended way to compare two offers for the same audience.
No offer is shown, and the buyer goes straight to the thank-you page. If you want a floor, give one funnel a global rule - that is exactly what global is for.
Three ways, and any one hitting is enough: a specific variant, a SKU as text, or a product id meaning any variant of it. Use the variant when size or colour changes the right offer, and the product when it does not.
On the order subtotal, against a minimum and maximum. Either bound can be left open, so "over $200" and "under $50" are both expressible.
Yes, and how it behaves depends on the mode: a single product, a bundle where everything is included together, or a choice where the buyer picks one or several. The layout adapts to how many items the step carries.
Each step branches on whether the buyer accepted or declined, so the path can differ. Follow an accepted offer with a bigger one, and a declined offer with something cheaper.
Restraint pays here. Every extra step is another page between the buyer and their confirmation, and the second decline is usually where goodwill ends.
The step is skipped and the buyer goes on to the next one. The funnel keeps running rather than stopping, and it starts offering the step again by itself once the product can be sold. Nothing needs switching off and nothing needs switching back on.
This is worth knowing before you read a report. A step that spent a week sold out has fewer views than the steps either side of it, and that is a supply fact rather than a result about the offer.
Charging for shipping on an upsell
Shopify ships post-purchase items free, which means the carriage on an accepted offer comes out of your margin unless you say otherwise. This is where you say otherwise, and what each of the three answers actually does.
Because that is Shopify's own behaviour, not a setting of ours. An accepted post-purchase offer becomes its own shipment on the order, and Shopify prices those extra shipments at zero. The order page says so in as many words: the first shipment carries the carrier's rate and the rest read Free.
You should see: On the next test order, the accepted offer's shipping appears on the shopper's confirmation instead of reading Free.
This is the one control here that changes what a shopper is charged rather than what they see. Whatever it decides is shown on the offer page before they accept, and the same figure is what the card is charged: the page cannot quote one number and take another.
What the extra weight adds to the parcel, at the per-kg rate your own orders show. It reads your store's real orders, fits the relationship between weight and what carriers charged you, and applies it to the item being offered. It is an estimate from your own history, not a live quote.
There is no live carrier quote available here and there cannot be. Shopify never re-rates a post-purchase shipment, so there is no rate to ask for. The line under the control tells you what Magic will do on your store specifically, including when the honest answer is that your rates do not vary with weight and it will therefore charge nothing.
It is one box that means two things, depending on the mode beside it. On Flat it is the rate and it is charged every time. On Magic it is what gets charged whenever the estimate cannot be worked out, which is an ordinary occurrence rather than an exotic one.
Leaving it empty on Magic means those offers ship free and nobody is told, so the app warns about it rather than accepting it quietly. A fallback is not a nicety on this control.
Yes. Each product in the step has its own Shipping box, and what you put in it beats the step's setting for that product alone. This is what lets a step sell something heavy beside something that rides along in the same box.
A step offers a snowboard and a sticker pack. On Magic the snowboard is estimated from its weight, and the sticker pack is set to 0 so it rides along free.
Empty and 0 are different answers, and the difference is deliberate. Empty means you have not said, so the step decides. Zero means you have said, and it means free. A control where blank quietly meant free would make every item you had not thought about ship at your expense.
The setting is per step, and several things downstream of it can send the offer back to free.
Designing the offer page
The post-purchase page is drawn by Shopify from a fixed set of components, not by your theme, so what can be styled is narrower than anywhere else you have designed. This is where that line falls, including the parts that are not ours to move.
Blocks and their order, the layout, text size and weight, three semantic text colours, image framing and size, alignment, and the space around each block. You place blocks in a list; the shopper's page draws them top to bottom in that order. Alongside the text blocks there is a countdown timer, a row of trust badges, and a hero image with four sizes.
The preview is the same design drawn by a second renderer, not a screenshot of the real page. It is accurate about layout and copy, and it cannot show you the acceptance banner a shopper sees on step two, because whether that appears depends on the path they took rather than on this step's design.
The same screen sets what is actually being sold. Which variants the shopper may choose between, whether the quantity is fixed or theirs to pick and how high it can go, a compare-at price, a discount, and how any saving is worded.
The quantity maximum is a real limit rather than a menu. The server clamps what it charges to that number, so lowering it genuinely stops a larger quantity being bought, which a dropdown alone would not.
Because Shopify's post-purchase components do not have them. This is not a feature we skipped: there is no margin property on any component, no line height, letter spacing, font family or point size, and no free colour anywhere. Text colour is three named values (success, warning, critical) that Shopify's own theme decides the look of.
Worth separating two things that sound alike. PADDING is room around a block, and you have it. SPACING is the gap between blocks, or between the lines of a bullet list, and that is fixed by Shopify - the Space and Indent controls cannot reach it, and a Spacer block is the way to open a gap. We would rather show you a short list of controls that work than a familiar-looking one that quietly does nothing on a real order.
The wording is Shopify's and cannot be changed, only translated: the accept button reads Pay now and the total, the decline reads Decline upsell offer. The POSITION is yours. Every page ends with a pair, and you can place a second pair partway down a long page so the price and the buttons stay in view.
We previously said the position was fixed too. That was our mistake rather than a Shopify rule - the only placement rule is that decline sits under accept and stays less prominent. If you were told the buttons could not be moved, this is the correction.
Not today. Nothing may take a buyer out of the post-purchase flow, and a second action competing with Accept works against the one decision the page exists for. Some competing apps do ship a linked image that opens a new tab; we have decided against it rather than been prevented, and will revisit if enough merchants need it.
Worth knowing what it would cost: the buyer leaves a page that is mid-transaction, and in an in-app browser a new tab may not open at all. That is the trade, and it is why this is a judgement rather than a limitation.
The first offer is fetched at checkout and parked, so reloading the post-purchase page replays that snapshot rather than asking us again. Every later step is live. This is deliberate and correct for the shopper - the offer promised at checkout is the offer honoured - but it means iterating on step one needs a new test order each time.
Steps two and beyond can be re-tested by reloading and advancing, so build the long designs there if you are iterating quickly.
Understanding the results
The hard part of testing is not collecting numbers, it is knowing when a difference is real. Most of this section exists to stop you acting on one that is not.
A measure of how surprising your result would be if the two versions were actually identical. High significance means the gap you are seeing would rarely happen by chance alone - it does not mean the gap is large, or that it will hold.
Flip two fair coins 10 times each. One will usually 'win'. Flip them 10,000 times each and they converge. Significance is the arithmetic that tells you which of those two situations you are in.
That there is not yet enough traffic for a lead to mean anything. We show the sample size needed to reliably detect a difference of a given size; below it, a lead is as likely to be noise as signal, so no winner is named. A page test is sized against a 10% difference by default. Funnel and pre-purchase offer tests are sized against 25%, because they are judged on far fewer events - orders and shoppers shown an offer rather than page views - and a 10% target there would ask for traffic no store has. You can set your own figure per test.
At a 2% conversion rate you need roughly 15,000 visitors per version to detect a 10% difference. With 400 each, a version showing 3% against 2% is not a 50% winner - it is four orders against three.
The banner says what is still missing instead of showing a number that looks decisive. An underpowered test is not broken - it is unfinished.
Because checking repeatedly and stopping the first time a version crosses 95% declares a winner on two IDENTICAL pages about half the time. We measured that. A reliability-triggered test runs until both versions carry enough traffic, then judges once.
No, it is a result, and an actionable one: it tells you the change does not matter and your effort belongs elsewhere. Inventing a winner from a rounding difference is how you roll out a change that never worked.
Revenue per visitor, usually. It stays correct when versions differ in both how often they sell and how much they sell, and a version can convert less often and still deservedly win.
A version converting 10.3% against 13.8% still leads if its buyers spend enough more: $342.97 per visitor against $308.78. Fewer orders, bigger baskets, more money.
It is only trustworthy on an even split, because the version shown to more people wins by construction. Every figure on the report is one version's, compared against another - none of them are test-wide totals.
How likely each version is to be the genuinely better one. It is the question people think a p-value answers. 60% is not a winner; it is a coin with a slight lean.
95% means that if you rolled this change out, you would be right about 19 times out of 20. 60% means 3 times out of 5 - barely better than guessing, and not worth a deployment.
What a sale is worth. Refunded orders are excluded by default, because money that came back is not evidence a version sells. Shipping and tax are included by default, so the figures reconcile with the order totals Shopify already showed you.
Gross profit is revenue minus your product costs, after refunds and before the payment fees and shipping we cannot see. It needs a cost per item, which Shopify only has if you have entered one; without it the figure would just be revenue under a second name, so the tile says what is missing rather than printing the same number twice under two labels.
A shopper actually seeing an offer - and the word covers two counts, each labelled where it appears. Funnel views (the funnel list, the home) count shoppers who ENTERED a funnel, one per run. Step views (the flow chart, the report's Views column, an order's Offered line) count who was SHOWN that specific step, so later steps read lower: a buyer who takes upsell 1 may never see upsell 2. Step views have been counted since the Aug 2026 update; where history predates that, or an active filter is one a view cannot answer, the report shows a dash whose hover names the exact reason - never a number that is quietly too small.
As their own rates per version, so a variant that sells well and returns badly is visible rather than flattering. Refunded orders are already out of the revenue figures by default.
It adds money from repeat subscription charges to the version that acquired the subscriber. Off by default, because a renewal is not a result of the test the way the first order is: the test won the customer, and the renewals are what the product did afterwards.
A renewal is never counted as a sale for the test itself. It carries no version of its own and is linked back to the order that acquired the subscriber, so it can only ever be shown through this toggle. That is deliberate: it is what keeps a stopped test stopped, instead of letting an archived experiment go on collecting revenue in every base figure. A renewal we cannot trace to exactly one version reads as untraced rather than being assigned to the likelier one.
What the offers have earned, and how orders that took one compare with orders that did not. Extra revenue is money from accepted post-purchase and pre-purchase offers since you installed. The three averages beside it are your order value with an offer taken, without one, and across the store as Shopify counts it.
The two average order values are a comparison between groups of shoppers, not a measurement of what the offers did. People who accept an upsell were already more willing to spend, so some of that gap belongs to them rather than to you. The number that answers what the offers earned is a test, and that is what the tests page is for.
From what was actually seen. The chips list only the channels your own visitors arrived through, so the set differs between stores and grows as new sources appear. The journey on an order shows where that buyer first landed, what brought them, and what they did in order.
A channel missing from the list means nobody arrived that way while the app was watching, which is not the same as nobody arriving that way. Traffic before you installed, or while the theme app embed was off, was never seen and cannot appear here.
When something looks wrong
Organised by what you are seeing, not by what is broken. Each one lists the possible causes in the order they are worth checking, because the common cause is rarely the interesting one.
Something between the visitor and us is not connected. These are worth checking in order.
Visitors are being assigned, so the attribution token is not surviving the trip to the order.
Either Shopify never handed the buyer to us, or no funnel wanted that order.
Almost always the priority hierarchy doing exactly what it should.
Usually correct behaviour rather than a fault.
Orders are being counted against visits that were never recorded.
Almost always a visitor who genuinely saw both.