Docs menu · Your own proxy container

Docs / COD Order Protection / Set up GTM

COD Order Protection

Your own proxy web container

Add COD Order Protection to a web container that sends events to PixelFly without sGTM.

Before you start

  • COD Order Protection is on in PixelFly — Step 1.
  • Have your API Key (pf_live_…) ready.

Shortcut worth taking: import our proxy container into a spare workspace. Everything below is already built there, so you can open each piece and copy its settings.

What you are building, in plain terms

Everything here is one of two jobs:

  1. Hold — when a COD purchase happens, send it to PixelFly instead of your ad platforms.
  2. Block — stop every purchase tag firing for that order.

There is no third “return” step as there is with sGTM: with no server container to send it back through, the confirmed order leaves PixelFly through Receivers instead.

1. Import the tag template

Templates → Tag Templates → New → ⋮ → Import, choose the file, then Save.

Download pixelfly-cod-protection-web.tpl

2. Create the hold tag

Tags → New, tag type PixelFly COD Protection (Web). Fill in:

Field in the tagSet it to
Server hostname / base URLhttps://track.pixelfly.io, or your own tracking domain. No path on the end.
API KeyYour container's API Key (pf_live_…)
How to read order fieldsAuto from dataLayer (Recommended)
Skip if payment_method is not CODTicked

Worth adding: under Additional fields to store, add a row named ga_session_id holding the GA session ID. It lets GA4 place the confirmed purchase in the same visit the ad brought, rather than as a fresh session days later.

3. Build the COD hold flag

Your trigger needs a way to say “this order is COD and COD is switched on”. Build it once, as variables, and one switch then controls everything.

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 nameData Layer Variable Name
dlv - ecommerce.payment_methodecommerce.payment_method
dlv - payment_methodpayment_method
dlv - ecommerce.payment_typeecommerce.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 key = String({{PixelFly API Key}} || '');
  if (key.indexOf('pf_live_') !== 0 || key === 'pf_live_REPLACE_ME') 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. It also refuses to hold while the API Key is still a placeholder, so an unfinished container cannot swallow a real order. 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.

4. Create the hold trigger

Triggers → New → Custom Event, event name purchase, fires on Some Custom Events with:

VariableOperatorValue
cJS - PixelFly COD holdequalstrue
📌 prevention_reloadequalsfalse

Set it as the hold tag’s firing trigger. This same trigger is what you add as an exception everywhere in the next step.

5. Block every purchase tag — the step to be thorough about

Add the trigger from step 3 under Exceptions on each of these:

  • ☐ Meta Pixel
  • ☐ GA4 purchase
  • ☐ Google Ads conversion
  • ☐ TikTok Pixel
  • ☐ The PixelFly server-side tag
That last one is the one people forget. Leave the PixelFly tag out and PixelFly receives the purchase at checkout anyway — exactly what the hold exists to prevent, and it will look like everything is working.

6. Add Receivers in PixelFly

One Receiver per platform the confirmed order should reach: Meta, GA4, TikTok, Google Ads. With no server container there are no server tags to run, so Receivers are the only way out.

GA4 needs a decision. A held COD order has no browser left to fire from when you confirm it days later, so a browser GA4 purchase tag will only ever see prepaid orders. Send GA4 purchases through PixelFly with a GA4 Receiver instead — and then remove the browser GA4 purchase, or every order counts twice.

Next

Publish, 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 5 is invisible until you watch a COD order go through and see which tags fire.

We use cookies to enhance your experience, analyze site traffic, and for marketing purposes. By continuing to use our site, you consent to our use of cookies. Learn more