I offered 5% off to move shoppers off cash on delivery. The numbers surprised me
In Pakistan, cash on delivery isn't one payment option among many. On the store I run tech for, a bookstore, it's 655 of the last 855 orders. That's 77%. COD is expensive. Couriers collect the cash and pay it out later,
In Pakistan, cash on delivery isn't one payment option among many. On the store I run tech for, a bookstore, it's 655 of the last 855 orders. That's 77%.
COD is expensive. Couriers collect the cash and pay it out later, refused parcels come back at your cost, and you hold stock for orders that may never be paid for. So I built the obvious fix: pay online and save 5%.
The data was not what I expected.
What I built
The shopper ticks a toggle on the product page or in the cart drawer. Two Shopify Functions read that choice:
- An order discount (
purchase.order-discount.run) that takes 5% off the order - A payment customization (
purchase.payment-customization.run) that hides COD, so nobody takes the discount and then pays cash anyway
Both read one shared signal, a cart attribute:
await fetch('/cart/update.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ attributes: { prepay_discount: 'true' } }),
keepalive: true,
});
The toggle on the product page and the one in the cart drawer stay in sync through a single document event, so they never disagree.
The bug that only shoppers found
Tick the toggle, hit Buy Now straight away, and the page left before the /cart/update.js request finished. The box looked ticked, but checkout had no discount and still showed COD.
keepalive alone didn't fix it. What did: keep the in-flight request as a promise, and hold the form's submit until it settles.
form.addEventListener('submit', async (e) => {
if (!pending) return;
e.preventDefault();
await pending;
form.requestSubmit(e.submitter);
});
One more lesson: /cart/clear.js empties the items but keeps cart attributes. My first clean test passed only because an old attribute was still sitting in the cart. I retested in a brand-new browser profile.
The numbers (last 30 days)
| Checkouts | Started | Completed | Conversion |
|---|---|---|---|
| Opted in to pay online | 126 | 61 | 48% |
| Everyone else | 973 | 794 | 82% |
The shoppers I gave money to finished checkout far less often than the ones who paid cash.
A second detail surprised me more. 98 orders were paid by card or wallet, but only 61 used the discount. So 37 people paid online and never took the 5%. The people happy to prepay weren't the ones the discount was moving.
What I think is happening
To be clear, this isn't an A/B test. It compares two groups that chose themselves. My guesses:
- Removing COD removes the safety net. At checkout, a shopper who opted in sees one option: the gateway. If a bank OTP fails or a wallet times out, there's no fallback, so they leave.
- 5% doesn't buy trust. For someone who has never ordered from you, paying first is a risk. A small discount doesn't cancel it.
- Abandoned checkouts include retries. Some of those 65 may have come back and paid COD in a fresh checkout.
What I couldn't build
The cleaner design is a discount that applies only when the shopper actually pays online, with COD still visible. That needs to know the chosen payment method, which needs checkout extensibility, which on a custom app needs Shopify Plus. This store isn't on Plus.
So the opt-in toggle is the best version available. The store owner looked at the numbers and kept it, because 61 prepaid orders are 61 orders that can't be refused at the door.
What I'm changing next
This part is planned. It isn't live yet.
The obvious fix is to stop hiding cash on delivery. Bank OTPs fail in Pakistan more often than anyone likes, and a shopper whose payment just failed should still have a way to finish the order.
There's a catch, and it's the Plus limit from above: the discount can't see which payment method gets picked. With COD visible, someone can tick the toggle, take the 5%, and still pay cash. I don't know how many will. Nobody does until it runs.
So the test looks like this:
- COD stays visible next to the toggle, with one line under it: "Payment didn't go through? Cash on delivery is still available."
- For a fixed window, I compare by payment choice: checkouts started, checkouts completed, and how many took the 5% but paid cash anyway.
- If completion rises by more than the leaked discounts cost, it stays. If not, COD goes back to hidden, or the offer changes.
I'll post the result either way.
If you're about to build this
- Measure checkout completion by payment choice before you launch, not just after.
- Don't hide the fallback payment method without testing what happens when the gateway fails.
- Check whether your store's plan supports the clean version before you write any code.
I build checkout and discount logic for Shopify brands. More at fahadazhar.site. If you've moved shoppers off COD in Pakistan, India or the Gulf, I'd like to know what actually worked.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.