- Browser pixels miss orders that finish outside the normal checkout, such as cash on delivery forms, external checkouts, order forms and funnels.
- A cash on delivery order stays unpaid until the courier collects the cash, so a setup that sends purchases on payment reports it late or never.
- Meta rejects events with an event time more than 7 days in the past, so most cash on delivery stores should send the purchase when the order is created or confirmed.
- Refused orders stay in Ads Manager, so divide your break-even ROAS by the share of orders that get paid.
- In ROASaurus, choose External / COD checkout in Store settings to send Shopify purchases when the order is created.
Cash on delivery conversion tracking means sending each purchase to your ad platforms from the order itself, on the server, instead of relying on a browser pixel on a thank-you page. Orders from cash on delivery apps, external checkouts, order forms and funnels often finish outside the normal checkout, so the pixel misses them or reports them at the wrong moment.
This guide explains why those orders go missing and why the payment status matters. It compares three moments to send the purchase: when the order is created, when it is confirmed and when it is paid. It also covers refused orders and refunds, and how ROASaurus handles external checkouts.
Why do pixels miss cash on delivery orders?
A browser pixel reports a purchase when the buyer reaches a page that runs it. That is usually the thank-you page of your checkout. Every order path that skips this page, or runs it on another domain, loses the purchase event or the ad click that belongs to it.
| Order path | Where the order is completed | What goes wrong with a browser pixel |
|---|---|---|
| Cash on delivery form app | A form on the product page; the app creates the order | The store checkout never runs, so its purchase event never fires |
| External checkout | A checkout run by another app, often on another domain | Your pixel may not load there, and the ad click cookies stay on your store domain |
| Order form or manual order | A form, a chat or a phone call; staff enter the order | There is no browser session for a pixel to see |
| Funnel with its own checkout | A landing page builder on its own domain | The ad click is on one domain and the purchase on another |
The order itself is never lost. It ends up in your webshop or your funnel tool with an order id, an amount, an email address and often a phone number. That is why the reliable fix is to send the purchase from the order data, server-side, and to carry the ad click along to the order. Server-side tracking on Shopify explains the general method. This post covers the orders that need extra care.
Some cash on delivery apps also fire their own pixel events. If they use the same pixel as your server-side tracking but a different event id, the platform counts the order twice. Turn those app events off, or follow the steps to prevent double counting.
Why the payment status matters
A card payment is captured in the checkout, so the order is paid the moment it exists. A cash on delivery order is different. Shopify lists cash on delivery as a manual payment method, and these orders stay Payment pending until you mark them as paid. In practice that happens after the courier collects the cash, which can take days. Some orders are never paid at all, because the buyer refuses the parcel or is not at home.
Many tracking setups send the purchase when the order is paid, because for card orders that is the safest moment. For a cash on delivery order, the same rule means the purchase arrives days late or never. The log below shows a typical case on a store that sends purchases on payment.
14 Oct 10:02 orders/create #4821 $50.00Order placed with cash on delivery, payment pending14 Oct 10:02 PurchaseNot sent: this store sends purchases when paid16 Oct 09:40 #4821 shippedParcel handed to the courier22 Oct 15:12 orders/paid #4821Marked as paid after the courier paid out22 Oct 15:12 Purchase event_time=14 OctRejected: the event time is more than 7 days oldWooCommerce works the other way around. It sets cash on delivery orders to Processing until the payment is collected, except orders with a downloadable product, which go to On hold. A setup that sends orders in Processing therefore sends them as soon as they are placed. Check which status your platform gives these orders before you decide anything else.
Created, confirmed or paid: when should you send the purchase?
There are three moments to choose from. Each one trades speed against accuracy.
| Send the purchase when the order is | Typical delay after the order | Includes orders that are never paid | Main risk |
|---|---|---|---|
| Created | Seconds | Yes: refusals and fake orders | Ads Manager shows more revenue than you collect |
| Confirmed by your team, by phone or message | Hours to a day | Refusals only | Needs a confirmation step that can trigger the event |
| Paid or delivered | Days to weeks | No | Late or lost: the ad click falls outside the attribution window, or the event is rejected |
Why late purchase events are a problem
Ad platforms credit a purchase to an ad only when it happens inside the attribution window after the click. With a 7-day click attribution setting on Meta, a purchase counts for an ad up to 7 days after the click. Meta's Conversions API also refuses events whose event_time is more than 7 days in the past, and it rejects the whole request when one event is too old. A purchase that is sent on payment therefore has two bad options:
- Send it with the time of the order. If the payment came more than 7 days later, Meta rejects it.
- Send it with the time of the payment. Meta accepts it, but the ad click may now be more than 7 days old, so the purchase is not credited to the ad.
Speed also matters for optimization. Campaigns that optimize for purchases learn from the purchases you send. A signal that arrives a week late tells the algorithm about last week's audience.
Which moment to choose
For most cash on delivery stores, sending on creation is the better default. The signal is immediate, the click is still inside the window, and nothing is rejected. The cost is that refused orders count as purchases in Ads Manager. You correct for that in your targets, as the next section shows.
If your team confirms each order by phone or message before shipping, the confirmation is a good middle ground. It removes fake orders, and it usually happens within a day. Use it when your own system can send the purchase at that moment, for example through an API. Fake orders matter more than they seem: if the ad platform learns from them, it looks for more people who place orders and do not pay.
How to handle refused orders, cancellations and refunds
Once a purchase is sent, it stays in Ads Manager. A parcel refused at the door does not remove it. ROASaurus does not send refunds or cancellations to Meta, TikTok and most other ad platforms either, so the revenue they report includes orders you never collected. Only Google Ads and Microsoft Advertising values can be corrected after a refund, when value corrections are on. The fix is to measure your refusal rate and adjust the numbers you compare against.
Here is a worked example for one month. To keep it simple, prices are without VAT.
| Line | Amount |
|---|---|
| Orders sent to Meta on creation | 200 orders × $50 = $10,000 |
| Refused at the door (18%) | 36 orders, $1,800 |
| Delivered and paid | 164 orders, $8,200 |
| Product cost of delivered orders (30%) | −$2,460 |
| Shipping for all 200 parcels at $6 | −$1,200 |
| Return shipping for 36 refused parcels at $6 | −$216 |
| Cash collection fee, $1.50 per paid order | −$246 |
| Handling, $1 per order | −$200 |
| Gross profit | $3,878 |
With $3,000 of ad spend, the same month looks very different depending on which revenue you use:
The gross margin rate on delivered revenue is $3,878 ÷ $8,200 = 47.3%, so the break-even ROAS is 1 ÷ 0.473 = 2.11. That break-even applies to delivered revenue. Ads Manager also counts the refused orders, so you have to translate it before you compare. Break-even ROAS explains the base formula.
- Break-even ROAS
- 1 ÷ gross margin rate, calculated on paid orders only.
- Payment rate
- The share of orders that are paid, for example 82% when 18% are refused.
2.114 ÷ 0.82 = 2.58. A campaign that shows less than 2.58 in Ads Manager loses money, even though it looks well above 2.11.
You can check the result directly. The ads break even when the ad spend equals the gross profit of $3,878, and $10,000 of reported revenue ÷ $3,878 = 2.58.
Refusal rates often differ by region, product and campaign. Calculate them per campaign when you have enough orders, and use the POAS calculator to check the result on gross profit.
Refunds and cancellations in your own numbers
On Shopify, cancellations and refunds reach ROASaurus by webhook, so profit analytics counts refunds against revenue. You choose whether cancelled orders that were never fulfilled count, and by default they are left out. A cancelled order that was already shipped stays in, because its shipping still cost you money. For recent orders that may still come back, the Predicted returns setting holds back an expected share of sales until real refunds arrive.
How ROASaurus handles cash on delivery and external checkouts
Shopify
Each Shopify store has a Checkout type in Store settings, with two options:
- Shopify checkout: the purchase is sent when Shopify reports the order as paid, through the
orders/paidwebhook. - External / COD checkout: the purchase is sent when the order is created, through the
orders/createwebhook. Use it for cash on delivery, and for external checkouts, such as CheckoutChamp or Checkify, that create the Shopify order themselves.
The setting applies to every order in the store. Card orders that are paid in the checkout are created and paid at almost the same moment, so the choice mainly changes when unpaid orders are sent. Each order is sent once. When the paid update for a cash on delivery order arrives later, no second purchase is sent.
The purchase uses the order's own data: the amount, the currency, the products, and the buyer's email, phone and address, hashed before they leave. ROASaurus matches the order to the ad click through the visitor id that its script saves on the cart. When an order does not carry it, ROASaurus tries, in this order: the cart and checkout tokens, the purchase event from the checkout, an email the visitor gave earlier on your site, and the same IP address and browser shortly before the order, when exactly one visitor fits. The Shopify integration page has the full setup.
WooCommerce and other webshops
The WooCommerce plugin sends an order once it reaches Processing or Completed. Cash on delivery orders reach Processing when they are placed, so they are sent right away. Bank transfer orders are sent when they move to Processing. The other webshop connectors send an order at these moments:
- Wix, Squarespace, Ecwid, Lightspeed, CCV Shop and Shopware: when the payment status of the order is paid.
- BigCommerce: when the order reaches a status such as Awaiting Fulfillment, Awaiting Shipment, Shipped or Completed.
- Magento: when the order state is Processing or Complete.
- PrestaShop: when the order is marked as valid.
- Custom webhook and orders API: when your own system sends the order.
Check how your platform marks cash on delivery orders. If they stay unpaid until the courier pays out, their purchases arrive late.
Funnels and your own checkout
- Add the script to the funnel pages
Use the same store key as your webshop, so a visitor keeps one visitor id on both domains.
- List your domains
In Store settings, add the funnel and store domains under Linked domains. Links between those domains then carry the visitor id and the click ids.
- Send the orders through the orders API
If the funnel takes the order itself, post each order to
/api/v1/orderswith the_tlcookie asvisitor_id. Your system decides the moment: on creation, on confirmation or on payment. - Check the Event log
When the first real orders arrive, open the Event log and check that each one was sent to every connected platform.
POST /api/v1/orders
X-Roasaurus-Key: <store key>
Authorization: Bearer <API key>
Content-Type: application/json
{
"order_id": "F-20931",
"created_at": "2026-10-20T14:05:00Z",
"total": 50.00,
"currency": "USD",
"email": "buyer@example.com",
"phone": "+15555550123",
"country": "US",
"visitor_id": "<value of the _tl cookie>",
"click_ids": { "fbc": "<_fbc cookie>", "fbp": "<_fbp cookie>" },
"client_ip": "203.0.113.7",
"user_agent": "Mozilla/5.0 ...",
"items": [{ "variant_id": "SKU1", "quantity": 1, "price": 50.00 }]
}The orders API keeps the first copy of each order id. If your system calls it on creation and again on payment, only the first call sends a purchase. More detail is in funnels and multiple domains.
Common mistakes to avoid
- Waiting for the paid status on a cash on delivery store. Purchases arrive a week late or not at all, and late events are rejected or fall outside the attribution window.
- Comparing Ads Manager ROAS with a break-even made for paid orders. Divide the break-even by your payment rate first.
- Two sources for one purchase. A form app's own pixel and your server events count the same order twice. See duplicate purchase events.
- Losing the click between domains. A funnel on another domain needs the script and linked domains, or its orders arrive without an ad click.
- Leaving manual orders out. Phone and chat orders have no browser session, but they still have an email address or a phone number. Sent from the order data, they give the ad platform something to match.
Sources
- Meta for Developers: Server Event Parameters
- Shopify: WebhookSubscriptionTopic (GraphQL Admin API)
- Shopify Help Center: Manual payment methods
- Shopify Help Center: Understanding your order statuses
- WooCommerce: Cash on Delivery documentation
In ROASaurus
- Shopify integration
- WooCommerce integration
- Help: funnels and multiple domains
- Profit analytics in ROASaurus