Docs menu · Your own sGTM containers
Getting Started
Integrations
Server-side GTM
COD Order Protection
Advanced Features
Downloads
Tools & Support
Docs / COD Order Protection / Set up GTM
COD Order Protection
Your own sGTM containers
Add COD Order Protection to web and server containers you already run, without replacing them.
Before you start
- COD Order Protection is on in PixelFly, with a fire path chosen — Step 1.
- Have ready: your API Key (
pf_live_…) and your Hold URL. - Publishing rights on both the web and the server container.
Shortcut worth taking: import our web and server containers into a spare workspace. Every piece below is already built there, so you can open each one and copy its settings instead of typing them.
What you are building, in plain terms
Three jobs. Everything below is one of these:
- Hold — when a COD purchase arrives, send it to PixelFly instead of your ad platforms.
- Block — stop your normal purchase tags from firing for that order.
- Return — days later, when you confirm the order, let it come back in and fire those same tags.
Job 2 is the one people get wrong: miss a single tag and that platform still counts unconfirmed COD orders, which defeats the whole setup.
Server container
1. Import the tag template
Templates → Tag Templates → New → ⋮ → Import, choose the file, then Save.
Download pixelfly-cod-protection.tpl2. Create the hold tag
Tags → New, tag type PixelFly COD Protection. Fill in:
| Field in the tag | Set it to |
|---|---|
Server hostname / base URL | Your Hold URL's host, without /cod/hold |
API Key (X-PF-Key) | Your container's API Key (pf_live_…) |
How to read order fields | Auto from event (Recommended) |
Skip request if payment_method is not COD | Ticked |
Auto reads transaction_id, value, currency, email, phone, fbp and fbc off the event. Only switch to Map fields manually if your event uses unusual field names.
3. Create the “hold” trigger
Triggers → New → Custom. Name it something like purchase – COD hold. Add all three conditions:
| Variable | Operator | Value |
|---|---|---|
| Client Name | equals | GA4 |
| Event Name | equals | purchase |
Event Data cod_hold | equals | true |
Set this as the hold tag's firing trigger.
No cod_hold flag coming from your web container yet? Use Event Data payment_method equals cod for now, and add the flag in the web container steps below.
4. Block your purchase tags — the step to be thorough about
Open every tag that sends a purchase and add the trigger from step 3 under Exceptions. Work through the list and tick them off:
- ☐ Meta / Facebook Conversions API
- ☐ GA4 server tag
- ☐ Google Ads conversion
- ☐ TikTok Events API
- ☐ Anything else of your own that sends a purchase
A tag you forget here will keep counting COD orders that were never confirmed — and it will look like it is working, which is why the test at the end matters.
5. Let confirmed orders back in
This is how the order returns days later, once you approve it.
-
Install the Data Client (by Stape), and set its accepted path to
/data.Easiest: in your server container, Templates → Client Templates → Search Gallery, search Data Client, pick the one by Stape, and Add to workspace. Then Clients → New → Data Client.
Or import the file if your container cannot reach the gallery: Templates → Client Templates → New → ⋮ → Import.
Download Data Client (.tpl)The Data Client is made and maintained by Stape, not PixelFly. We mirror it so the setup does not stall; prefer the gallery copy when you can, since it updates itself.
-
Create a second trigger — Triggers → New → Custom, name it purchase – COD confirmed:
Variable Operator Value Client Name equals Data ClientEvent Name equals purchaseEvent Data is_delayedequals true - Add this trigger as an extra firing trigger on the same purchase tags you just added exceptions to.
Each of those tags ends up with two firing triggers (your normal one, plus this) and one exception. That is correct: the exception blocks the order at checkout, and this trigger fires it when confirmed.
transaction_id and fails with “missing order_id” — while Preview still shows the ID, because Preview's Event Data is what arrived before transformations ran. That mismatch sends people hunting in the wrong place for hours.
Web container
6. Mark COD purchases, and keep the fields
On the GA4 purchase event you send to sGTM, add two parameters:
| Parameter | Value |
|---|---|
payment_method | The order's payment method |
cod_hold | "true" for a COD order while COD is switched on |
How to produce cod_hold
It is not a field your store sends — you build it in GTM from three small variables, so one switch controls the whole setup. Create them in this order (Variables → New).
a. Three Data Layer Variables
Type Data Layer Variable, one each, named exactly for the key they read:
| Variable name | Data Layer Variable Name |
|---|---|
dlv - ecommerce.payment_method | ecommerce.payment_method |
dlv - payment_method | payment_method |
dlv - ecommerce.payment_type | ecommerce.payment_type |
b. cJS - PixelFly payment method
Type Custom JavaScript. Different carts use different keys, so this takes whichever one is present:
function() {
return {{dlv - ecommerce.payment_method}} || {{dlv - payment_method}} || {{dlv - ecommerce.payment_type}} || '';
}
c. PixelFly - COD Protection
Type Constant, value off. This is your master switch — set it to on when the rest is built and published. Keeping it off while you work means a half-finished setup never holds a real order.
d. cJS - PixelFly COD hold
Type Custom JavaScript. This is the one the trigger and the cod_hold parameter read:
function() {
if ({{PixelFly - COD Protection}} !== 'on') return 'false';
var method = String({{cJS - PixelFly payment method}} || '').toLowerCase();
if (!method) return 'true';
var cod = method === 'cod'
|| method.indexOf('cash_on_delivery') !== -1
|| method.indexOf('cash on delivery') !== -1;
return cod ? 'true' : 'false';
}
It returns the strings 'true' / 'false', not booleans, so trigger conditions compare cleanly. Note an order with no payment method at all is held — an unknown method is more often COD than not.
e. 📌 prevention_reload
Type Custom JavaScript. Our COD trigger also checks this is false, so refreshing the thank-you page does not hold the same order twice:
function(){
return window.performance.getEntriesByType('navigation')
.map(function(nav){ return nav.type })
.includes('reload');
}
Skip this only if your purchase event already fires once per order by some other means.
And make sure these still reach the server — they are what the confirmed order will be sent with, days later:
- ☐
transaction_id,value,currency - ☐ customer
user_data(email, phone) - ☐
gclid
_gcl_aw cookie and send it as a parameter — your sGTM domain cannot read your site's cookies. Leave it out and Google Ads has nothing to credit the sale to when the confirmed order arrives.
7. Block the browser purchase tags
Same idea as step 4, now in the browser. Make one Custom Event trigger on purchase where cJS - PixelFly COD hold equals true and 📌 prevention_reload equals false, then add it under Exceptions on each:
- ☐ Meta Pixel
- ☐ Google Ads conversion
- ☐ TikTok Pixel
- ☐ Anything else of your own that sends a purchase
Next
Publish the server container first, then the web one.
Then place one prepaid and one COD test order. On a hand-built setup this test is not optional — a single missed exception in step 4 or 7 is invisible until you watch a COD order go through and see which tags fire.