The standard Shopify plus Meta setup fires a Purchase event the moment the thank-you page loads. For a prepaid store that is fine. For an Indian D2C brand where a large share of orders are cash on delivery, it means Meta is being told that every order placed is revenue, including the ones that get refused at the door, never answer the confirmation call, or come back as RTO. Meta then optimises toward the people who place orders most easily, which is not the same as the people who pay. I have already written about why server-side CAPI matters in general and how it compares with the pixel. This piece is narrower: how to restructure the event plan itself for COD orders so the signal Meta learns from matches money that actually reaches your bank.
Why the default Purchase event is wrong for COD
Shopify's native Meta integration and most pixel apps treat order creation as the purchase. On a COD order, order creation is a promise, not a payment. Between that moment and cash in hand there are several exits: the customer cancels on the confirmation call or WhatsApp message, the courier cannot reach them, they refuse the parcel, or the address turns out to be fake. Each of those becomes either a cancellation or an RTO, and you have usually paid forward shipping and often return shipping on it. Meta does not know any of that. It saw a Purchase with a value, attributed it to an ad, and will go looking for more people who behave the same way. If your COD buyers who refuse parcels share traits, impulsive late-night orders, certain pin codes, very low-intent placements, the algorithm will happily find more of them, because from its point of view they convert well. The fix is not a better pixel. It is deciding which business event deserves to be called a Purchase and then sending it from the system that knows when that event happened, which is your order or shipping backend, not the browser.
The event plan I use for COD stores
I split the journey into stages and give each one its own event. At checkout completion I send an event that tells Meta an order was placed, but not as the primary optimisation event. Some brands use a custom event such as OrderPlaced, others keep InitiateCheckout or AddPaymentInfo as the last browser-side signal. Prepaid orders are different: payment is captured, so they can send Purchase immediately, browser and server, deduplicated. For COD, Purchase is sent later from the server when the order reaches a status you trust. That status is a business decision. Delivered is the most accurate but the slowest. Confirmed, meaning the customer verified the order on a call, IVR or WhatsApp prompt, is faster and filters out a meaningful slice of junk. Shipped sits in between but still includes refusals. I usually start with confirmation for COD because it keeps the feedback loop short enough for Meta to learn, then compare it against delivered data monthly to see how much noise is left. Whatever you choose, document it, because everyone reading Ads Manager needs to know what a Purchase means in this account.
Respecting Meta's time window
The constraint that shapes this design is time. Meta's Conversions API documentation expects event_time to be no more than seven days in the past when the event is sent, and events older than that are rejected. COD delivery in metro pin codes can be quick, but in tier 2 and tier 3 locations, or when a courier reattempts, delivery can stretch past a week from the original click and order. If you wait for delivered status on every order, some genuine purchases will arrive too late to count. There are two practical answers. Use confirmation as the Purchase trigger so the event lands within days, and track delivery separately for your own reporting. Or send Purchase at delivery but set event_time to the delivery moment rather than the order moment, accepting that the conversion will appear later in reporting and may sit outside the attribution window for some clicks. I prefer the first approach for most accounts, because a short, consistent signal is worth more to the delivery system than a perfectly accurate one that arrives late or not at all.
Getting the data out of Shopify and your shipping stack
The server event needs a trigger. In Shopify, the order record carries the payment gateway and financial status, so you can tell COD from prepaid at order creation. For confirmation, the trigger usually comes from whatever tool runs your COD verification: a WhatsApp confirmation flow, an IVR tool, or a team marking orders in an app, which then tags the Shopify order or updates a metafield. For delivery, the signal comes from your shipping aggregator's status updates, which most aggregators can push by webhook. I route these into a server-side GTM container or a small cloud function that listens for the status change, looks up the original order, and builds the CAPI payload. That payload needs the customer data Meta uses for matching, hashed email and phone, plus the fbp and fbc values captured at checkout. Those two browser identifiers have to be stored on the order at the time it is placed, typically as note attributes or metafields, because they no longer exist when the server event fires days later. Without them, match quality on delayed events drops sharply.
Values, currency and partial outcomes
Send the value you actually expect to collect. For a COD order that is usually the order total minus any discount, in INR, and for consistency I exclude shipping charges unless the brand reports revenue including them. If a customer accepts part of a multi-item order, send the accepted value, not the original. If you use a confirmation trigger and the order later becomes RTO, Meta has no clean mechanism to reverse a Purchase it has already received, so do not invent one by sending negative values. Instead, accept that confirmation-based Purchase still carries some RTO noise and correct for it in your own reporting, which is exactly what RTO-adjusted ROAS is for. The other thing I watch is currency formatting. Server events sometimes ship values in paise or as strings with commas, and Meta will either reject them or record nonsense. Every payload should carry a numeric value and currency set to INR.
Deduplication when browser and server both speak
Prepaid orders will typically send Purchase from both the browser and the server, which means deduplication matters. Meta deduplicates when the two events share the same event_name and event_id, so the order ID or a value derived from it should be used as the event_id on both sides. COD orders are simpler in one way and riskier in another. If you move Purchase to the server only, you must also make sure the browser pixel is no longer firing Purchase for COD orders, or you will report the order twice: once from the thank-you page and once from the confirmation trigger, possibly with different event_ids. In practice this means editing the pixel logic, or the app settings, so the thank-you page fires Purchase only when the payment method is prepaid. After launch, check Events Manager for the Purchase event: you want to see server events for COD orders, browser and server for prepaid, and a deduplication indicator rather than counts that look roughly double your Shopify order report.
What to expect after you switch
Moving COD Purchase to a later, stricter trigger means reported conversions in Ads Manager will drop, sometimes noticeably, because you have stopped counting orders that never turned into revenue. That is the point, but it unsettles teams who watch platform ROAS daily, so I agree the change with the founder and finance before shipping it. Treat the first two to three weeks as a learning period. Campaigns optimising for Purchase may re-enter learning, and you should avoid stacking other big changes on top. Compare Meta's numbers against delivered revenue by week rather than by day. If the event plan is working, the gap between what Meta reports and what your finance team sees should narrow, and over time the mix of orders coming from Meta should shift toward customers who accept parcels. If you want a second opinion on the plumbing, this is the kind of build I scope as part of a CAPI setup.
FAQ
Neither extreme is perfect. Order placement includes refusals and fake orders, and delivery can fall outside Meta's seven-day limit for event_time. I usually trigger Purchase at customer confirmation, which removes much of the junk while keeping the event timely, and track delivery separately for reporting.
There is no reliable way to reverse a Purchase Meta has already received, and sending negative values is not a supported approach. The better path is to delay Purchase until the order is confirmed or delivered so fewer RTO orders are counted, then adjust ROAS for remaining RTO in your own reporting.
Yes. The pixel still captures browsing events, sets the fbp and fbc identifiers you need for matching, and can send Purchase for prepaid orders. For COD orders, it should stop firing Purchase on the thank-you page so you do not double count.