The idea of using Webhooks inside of Google Tag Manager occurred while working on a web tracking project that used Samcart for checkouts.
The problem is as follows:
When a purchase happens, Samcart creates two IDs: an Order ID and a Transaction ID.
- The Transaction ID goes into the dataLayer for GA4.
- The Order ID appears only in Samcart’s reports.
The two never travel together. GA4 has no Order ID, and Samcart has no Transaction ID. Without linking them, you can’t reconcile orders or send accurate refunds back into GA4.
The fix was to capture both at the source. In GTM, I fired a webhook on purchase to send the Transaction ID into Zapier. Separately, Samcart sent the Order ID into another webhook. Storing them side by side gave me the missing link.
With webhooks, GTM can talk to systems that never had a native tag. CRMs, support tools, finance systems, internal databases—you name it. Instead of relying solely on analytics platforms, you can make GTM feed data wherever you need it.
And the best part? You don’t need a developer to rewrite backend code. You can get started with a webhook tag in a few minutes.
How to send data from GTM to a webhook
I’m using Zapier webhooks but you can use any webhooks. Infact, you can also create your own webhook.
There are three main ways to do it. Each has trade-offs.
1. Quick & Dirty: GET via Image Tag
- The simplest hack: fire a Custom Image tag in GTM.
- The webhook URL includes your data as query parameters.
- Zero CORS headaches, fires reliably.
- Works when your payload is tiny (think: a transaction ID + timestamp).
This is the “just make it work” method. Perfect for lightweight signals and this is the one I used for my implementation.
In Zapier
- Create a Zap → Webhooks by Zapier → Catch Hook.
- Copy the catch URL.
In GTM
- Create Data Layer Variables for the fields you want to send (e.g. dlv – client_id, dlv – event_name, etc.).
- Create Tag → Custom Image
- Image URL (an example): https://hooks.zapier.com/hooks/catch/XXXXXXX/YYYYYYY/?event={{dlv – event_name}}&client_id={{dlv – client_id}}&value={{dlv – value}}
- Enable “Support document.write” = off.
- Create Trigger → Custom Event e.g. zap_event (or whatever you push).
On your site/app, fire it with:
dataLayer.push({ event: 'zap_event', event_name: 'form_submit', client_id: window.gtag && gtag.get ? gtag.get('get', 'client_id') : undefined, value: 123 });
Pros: dead simple, no CORS.
Cons: URL length limits; values visible in URL; GET only.
2. Better (POST) from web GTM
- Use a Custom HTML tag and POST a JSON body.
- With
navigator.sendBeacon
, you avoid CORS preflight issues and it still fires on page unload. - You can send more fields, structured neatly.
- Great for purchases, form data, or any event where you want richer context.
Think of this as the grown-up method: flexible, reliable, still easy to set up.
This lets you POST safely from the browser without preflight. Great for modest payloads.
In Zapier
Same as above: Catch Hook URL.
In GTM
- Make your Data Layer Variables as above.
- Tag → Custom HTML (fire on your custom event – this one is just an example)
<script> (function () { var url = 'https://hooks.zapier.com/hooks/catch/XXXXXXX/YYYYYYY/'; // Build a clean payload from the dataLayer variables you exposed in GTM var payload = { event_name: '{{dlv - event_name}}', client_id: '{{dlv - client_id}}', value: '{{dlv - value}}', page_url: '{{Page URL}}', user_agent: '{{User Agent}}' }; // Remove empty/undefined keys Object.keys(payload).forEach(function(k){ if (payload[k] == null || payload[k] === '') delete payload[k]; }); // Prefer sendBeacon; it avoids CORS preflight and works during unload var body = JSON.stringify(payload); var ok = false; if (navigator.sendBeacon) { var blob = new Blob([body], { type: 'application/json' }); ok = navigator.sendBeacon(url, blob); } if (!ok) { // Fallback with fetch using a CORS-safe content-type try { fetch(url, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, // text/plain avoids preflight body: body, mode: 'no-cors', keepalive: true }); } catch(e) {} } })(); </script>
- Trigger → Custom Event (e.g.,
zap_event
). - Fire it by pushing to the dataLayer (same as above).
Pros: POST, more fields, reliable on page unload, avoids CORS.
Cons: You can’t read the Zapier response (not needed for this use-case).
3. Most robust: route via Server-Side GTM (recommended for scale)
- Client → sGTM endpoint (first-party) → sGTM makes a server POST to Zapier.
- No browser CORS or adblockers.
- You can add auth/secret, throttle, retries, QA, PII scrubbing.
In sGTM: Create a Custom HTTP tag (or template) that POSTs to Zapier; trigger it from a custom client or GA4 MP event you forward.
High-level steps
- Set up sGTM container (App Engine / Cloud Run).
- In Web GTM, send a lightweight request to your sGTM endpoint (via GA4, or a custom HTTP tag to /zap-proxy).
- In sGTM, catch it and Tag → HTTP Request to Zapier with JSON body.
Field mapping & hygiene (important)
- Whitelist what you send. Don’t dump the whole dataLayer. Build a small payload object with only allowed keys.
- PII: Only send personal data to Zapier if you have consent and it’s in scope. Hash emails if you must send them.
- Consent: If you use Cookiebot, add a trigger condition so this fires only when the relevant consent category is granted (e.g., cookie_consent_update + your consent state).
- Dupes: Add an event_id (UUID) in your payload for deduplication on the Zapier side if your flow can double-fire.
- Error handling: With sendBeacon/no-cors you can’t read responses. If you need delivery audits, prefer sGTM so you can log and retry.
Testing checklist
- In GTM Preview, push a test event to dataLayer and ensure your tag fires once.
- In Zapier → Test Trigger → you should see the payload arrive.
- Add filters/paths in Zapier as needed.
- Publish GTM.
0 Comments