LIVE REFERENCE
GTM-SERVER // Rev 2024.Q4 // Updated March 2025

Move your tags
off the browser

sGTM is Google's server-side tag management solution that processes tag logic, event collection and data forwarding on a secure, scalable server — not in the visitor's browser. This fundamentally changes the data flow: instead of a browser executing 20+ third-party scripts, a single lightweight client-side hit is sent to your own cloud endpoint, which then fans out to all downstream vendors.

~40%
Page Weight Reduction
100%
1st-Party Data Control
~70%
ITP Signal Recovery
0
3P Scripts in Browser
Enrichment Potential
BROWSER LAYER gtag.js / web container GA4 Client No 3P Tags HTTPS POST sGTM CONTAINER Cloud Run / GCP europe-west2 Request Parser / Router Consent Mode v2 Check Data Enrichment Layer Firestore BigQuery Tag Triggers + Variables CMP / CONSENT OneTrust / Custom Consent State Object MOBILE SDK Firebase / GTM App VENDOR ENDPOINTS Google Ads googletag/gtag Measurement Meta CAPI graph.facebook.com/events Amazon DSP TCF 2.0 Required TikTok Events API S2S token + pixel_code Reddit Conversions ads-api.reddit.com moEngage Event API / Data API Amplitude HTTP API v2 / Batch AppsFlyer S2S ✱ SDK must stay on-device Google Analytics 4 — Measurement Protocol www.google-analytics.com/mp/collect BigQuery Raw Export / Data Warehouse Structured 1P event stream — enriched, consented

Clients
Clients parse incoming HTTP requests to the sGTM endpoint. The GA4 Client handles GA4 event payloads. Universal Analytics client handles ua-events. Custom clients can parse any arbitrary HTTP format — webhooks, Measurement Protocol direct hits, custom mobile payloads.
Tags
Tags are the outbound forwarders. Each vendor (Meta CAPI, TikTok Events API, GA4 etc.) has a corresponding server tag. Tags fire based on trigger conditions and variable values. All execution happens in the cloud — zero impact on browser performance.
Variables
Variables extract and transform data from the incoming event. They can read from the event model, HTTP headers, cookies, query parameters, and external services (via custom JavaScript). Enrichment variables call Firestore/BigQuery to append CRM or audience data in real-time.
Triggers
Triggers control when tags fire. Typically set to "All Events" or specific event_name matches. Critical: triggers also integrate consent mode — a tag can be conditionally gated behind ad_storage or analytics_storage consent states from the CMP.
Enrichment
The killer feature. Before forwarding to vendors, sGTM can append first-party data: hashed email from CRM, lifetime value, customer segment, user-agent parsing. This cannot be done client-side without exposing data to browser inspection.
Preview Mode
Server-side preview works via a dedicated preview URL. Unlike client-side GTM, debugging sGTM requires both the web container preview + server container preview active simultaneously. Cloud Run logs + GTM server preview panel combined give full observability.

01
Provision Cloud Run Service
Google provides a one-click setup that deploys sGTM to Cloud Run in your GCP project. Choose the region closest to your primary user base. For EMEA: europe-west1 (Belgium) or europe-west2 (London). Min instances: 1 (prevents cold starts). Set memory to 512Mi minimum.
02
Custom Domain Mapping
Map a subdomain of your own domain: gtm.yourdomain.com. This is critical — the endpoint must be first-party to avoid ad blockers. Safari ITP treats third-party origins differently; a first-party subdomain ensures cookies set by the server are classified as first-party.
03
Web Container Configuration
Update your client-side GTM web container to point transport URL to your sGTM endpoint. The GA4 configuration tag needs server_container_url set. This routes all gtag hits to your server first.
04
Auto-scaling & Costs
Cloud Run scales to zero when idle (saves cost) but causes cold start latency. For production, set min-instances=1. Typical cost at 10M events/month: ~$50–150/month depending on region and memory. Very cost-effective vs client-side tag overhead.
# Cloud Run deployment — sGTM gcloud run deploy gtm-server \ --image gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable \ --region europe-west2 \ --memory 512Mi \ --min-instances 1 \ --max-instances 10 \ --allow-unauthenticated \ --port 8080 \ --set-env-vars \ CONTAINER_CONFIG="YOUR_BASE64_CONFIG" \ RUN_AS_PREVIEW_SERVER=false # Custom domain mapping gcloud beta run domain-mappings create \ --service gtm-server \ --domain gtm.yourdomain.com \ --region europe-west2 # Verify HTTPS certificate provisioning gcloud beta run domain-mappings describe \ --domain gtm.yourdomain.com \ --region europe-west2
Preview Server: Deploy a second Cloud Run instance with RUN_AS_PREVIEW_SERVER=true for a dedicated debug endpoint. Never use production for preview — it leaks internal tag config via debug panels.

Media Vendor
Configuration Guide

Each vendor requires specific server-side tag templates, API credentials, identity signals, and consent gates. Below is the comprehensive configuration reference for all 8 major platforms.

Identity Matching: Server-side tags fire server-to-server with no browser context. You must explicitly pass hashed PII (SHA256 email, phone) and/or first-party cookie IDs as match keys. Without these, attribution rates drop dramatically.
🎯
Google Ads Conversion Tracking
Tag Template: "Google Ads Conversion Tracking" (built-in)
Required Credentials
Conversion ID (AW-XXXXXXXXX) + Conversion Label per action
Identity Signals
_gcl_aw cookie GCLID SHA256 Email SHA256 Phone
Consent Gate
ad_storage: granted ad_user_data: granted
Enhanced Conversions
Pass hashed email as sha256_email_address variable. Works with or without cookie match. Dramatically improves conversion modelling under ITP/consent loss.
Cookie Handling
sGTM reads _gcl_aw cookie from the HTTP request headers and passes GCLID to tag. Set cookie via sGTM response headers to extend lifetime to 400 days (1P domain).
// sGTM Variable — Extract GCLID from Cookie const getCookieValues = require('getCookieValues'); const gclid = getCookieValues('_gcl_aw')[0]; // Format: GCL.timestamp.GCLID return gclid ? gclid.split('.')[2] : undefined; // sGTM Tag — Enhanced Conversions payload { send_to: "AW-XXXXXXXXX/LABEL", value: {{event.value}}, currency: "GBP", transaction_id: {{event.transaction_id}}, user_data: { sha256_email_address: {{Hashed Email}}, sha256_phone_number: {{Hashed Phone}}, address: { sha256_first_name: {{Hashed FName}} } } }
Google Ads has native sGTM template support. Enhanced Conversions via sGTM recovers ~15-30% of conversions lost to ITP and consent decline.

📘
Meta Conversions API (CAPI)
Tag Template: "Facebook Conversions API" (community)
Required Credentials
Pixel ID + System User Access Token (not page token — use system user for server-side)
Identity Signals (rank order)
fbp cookie fbc (click ID) SHA256 Email SHA256 Phone External ID
Event Dedup
CRITICAL: Pass event_id on both browser pixel AND server tag. Meta deduplicates using event_name + event_id within 48h window. Without dedup, events are double-counted.
Consent Gate
ad_storage: granted OR model as data_processing_options
Event Match Quality
Target EMQ score >7.0. Use Events Manager diagnostics. Pass fbp+email+phone together for highest match rate. External ID (hashed CRM ID) adds resilience.
// Meta CAPI payload structure { data: [{ event_name: "Purchase", event_time: Math.floor(Date.now()/1000), event_id: {{event.event_id}}, // DEDUP KEY event_source_url: {{Page URL}}, action_source: "website", user_data: { em: ["<SHA256_EMAIL>"], // array ph: ["<SHA256_PHONE>"], fbp: {{fbp Cookie}}, fbc: {{fbc Cookie}}, external_id: {{Hashed CRM ID}}, client_ip_address: {{IP Address}}, client_user_agent: {{User Agent}} }, custom_data: { currency: "GBP", value: {{order.value}} } }] }
fbp cookie lifetime: Browser sets fbp as 90-day. Via sGTM you can set it as a server-side first-party cookie (Set-Cookie header) to extend to 400 days and survive ITP2.1 purging.

🛒
Amazon DSP — Conversion Tag
Custom HTTP Request Tag — No native sGTM template
Integration Method
Amazon provides a pixel/tag URL. sGTM fires an HTTP request to the tag URL server-side via Custom Image or HTTP Request tag.
⚠ TCF 2.0 Requirement
Amazon DSP requires IAB TCF 2.0 consent string to be passed with each event for GDPR compliance. The Transparency and Consent Framework string must encode Purpose 1 (Store/Access), Purpose 3 (Ad Selection), Purpose 4 (Content delivery).
TCF Consent Passing
gdpr=1 gdpr_consent={{TC_STRING}}
Identity
Amazon DSP relies on its own identity graph (Amazon Advertising ID). Limited cross-device matching outside Amazon properties. Hashed email can be passed as cbuid parameter in some integrations.
Event Endpoint
https://s.amazon-adsystem.com/iu3?pid=&event=...
🔴TCF 2.0 is MANDATORY for Amazon DSP. Without a valid TC string, Amazon will discard conversion events in GDPR regions. Your CMP (OneTrust) must expose the TC string, which sGTM reads from a cookie (typically eupubconsent-v2 or euconsent-v2) and appends to the Amazon pixel call.
// sGTM Custom Variable — Read TCF string const getCookieValues = require('getCookieValues'); const tc = getCookieValues('eupubconsent-v2')[0]; return tc || undefined; // Amazon DSP pixel with TCF appended URL: "https://s.amazon-adsystem.com/iu3" params: { pid: "<ADVERTISER_TAG_ID>", event: "conversion", v: "<order_value>", gdpr: 1, gdpr_consent: {{TCF String Variable}} }

🎵
TikTok Events API (S2S)
Template: "TikTok Conversions API" (community gallery)
Required Credentials
Pixel Code (8-char) + Access Token (Events API token from TikTok Ads Manager → Assets → Events)
Endpoint
https://business-api.tiktok.com/open_api/v1.3/pixel/track/
Identity
ttclid (URL param) _ttp cookie SHA256 Email SHA256 Phone
Event Dedup
Pass event_id on both browser pixel and server tag. TikTok deduplicates within a 48h window per event_name + event_id.
Consent
ad_storage: granted recommended. TikTok also supports limited-data mode for opted-out users.
// TikTok Events API payload { pixel_code: "ABCD1234", event: "CompletePayment", event_id: {{event.event_id}}, timestamp: "2024-01-01T12:00:00+00:00", context: { ad: { callback: {{ttclid}} }, page: { url: {{Page URL}} }, user: { email: {{SHA256 Email}}, // pre-hashed phone_number: {{SHA256 Phone}}, ttp: {{_ttp Cookie}} }, ip: {{Client IP}}, user_agent: {{User Agent}} }, properties: { currency: "GBP", value: {{order.revenue}} } }

🤖
Reddit Conversions API
Custom HTTP Request Tag — No official sGTM template (use community)
Endpoint
https://ads-api.reddit.com/api/v2.0/conversions/events/{account_id}
Auth
Bearer token in Authorization header. Generate via Reddit Ads → Conversions → Create Event Source.
Identity
_rdt_uuid cookie SHA256 Email SHA256 UUID
Event Names
Purchase, Lead, SignUp, AddToCart, ViewContent, Search, PageVisit, ViewContent
Dedup
Pass conversion_id to deduplicate browser pixel vs server events. 24h dedup window.
// Reddit CAPI payload { events: [{ event_at: "2024-01-01T12:00:00+00:00", event_type: { tracking_type: "Purchase" }, click_id: {{rdt_cid}}, // from URL ?rdt_cid= user: { email: {{SHA256 Email}}, uuid: {{_rdt_uuid Cookie}} }, custom_event_name: "purchase_web", products: [{ id: {{product_id}}, name: {{product_name}}, category: {{category}} }], value: {{order.value}}, currency: "GBP", conversion_id: {{event.event_id}} }] }

💬
moEngage Data API
Custom HTTP Request Tag — REST API integration
Use Case in sGTM
Forward behavioural events from web/app to moEngage for journey orchestration, push, email triggers. sGTM acts as the data router — normalises events before sending.
Endpoints
POST /v1/integrations/sdk/track — events
POST /v1/integrations/sdk/user/update — profile
Auth
Basic Auth with APP_ID:DATA_API_KEY (base64 encoded)
Identity
unique_id (CRM ID) — required. Maps to moEngage user. Must be consistent with mobile SDK identity.
On-Device SDK Note
Chase/enterprise: moEngage SDK remains on-device for push/in-app. sGTM only used for supplemental web event forwarding. Avoid double-tracking same event from SDK + sGTM.
// moEngage Track Event payload POST https://api.moengage.com/v1/integrations/sdk/track Authorization: Basic {{base64(APP_ID:KEY)}} Content-Type: application/json { type: "event", customer_id: {{CRM User ID}}, device_type: "web", actions: [{ action: "purchase_completed", attributes: { revenue: {{order.value}}, currency: "GBP", product_count: {{items.length}} }, platform: "web", created_at: "<ISO8601>" }] }
moEngage's EU data centre endpoint differs: https://api-eu.moengage.com. Always use the correct regional endpoint for GDPR compliance.

📊
Amplitude HTTP API v2
Custom HTTP Request or Community Template
Endpoint
https://api2.amplitude.com/2/httpapi (standard) or /batch for high volume
Auth
API Key in request body as api_key field. No header auth.
Identity
user_id (authenticated) device_id (anonymous)
Session Tracking
Amplitude's session_id is a Unix timestamp. sGTM must read the Amplitude session cookie (AMP_*) or generate/pass from the web SDK. Without session_id, funnel analysis breaks.
On-Device Conflict
Like moEngage — if Amplitude SDK is on-device, use sGTM only for server-authoritative events (confirmed purchases, backend triggers). Avoid duplication.
// Amplitude HTTP API v2 — event payload { api_key: "{{AMPLITUDE_API_KEY}}", events: [{ user_id: {{User ID}}, device_id: {{AMP Device ID}}, event_type: "purchase_completed", time: {{Timestamp MS}}, session_id: {{AMP Session ID}}, event_properties: { revenue: {{order.value}}, product_id: {{product_id}}, currency: "GBP" }, user_properties: { $set: { plan: {{user.plan}}, ltv: {{user.ltv}} } }, ip: {{Client IP}}, language: "en-GB", insert_id: {{event.event_id}} // dedup }] }

📱
AppsFlyer S2S Events API
Custom HTTP Request — S2S REST API
⚠ CRITICAL ARCHITECTURE NOTE
The AppsFlyer SDK MUST remain on-device. SKAN (iOS) and Google Play Referrer attribution requires the SDK to execute natively on the device. sGTM can forward supplemental server events, but cannot replace the on-device SDK for MMP attribution.
Valid sGTM Use Cases
Backend conversion events (subscriptions confirmed server-side), LTV updates, offline conversions, web-to-app journey events where app_id correlation is known.
Endpoint
https://api2.appsflyer.com/inappevent/{app_id}
Auth
DevKey in authentication header
Identity
appsflyer_id (required) — must be obtained from SDK on-device and passed server-side via your backend
// AppsFlyer S2S in-app event POST https://api2.appsflyer.com/inappevent/{app_id} authentication: {{AF_DEV_KEY}} Content-Type: application/json { appsflyer_id: {{AF Device ID from backend}}, customer_user_id: {{CRM User ID}}, ip: {{Client IP}}, eventName: "af_purchase", eventValue: { af_revenue: {{order.value}}, af_currency: "GBP", af_content_id: {{product_id}} }, eventTime: "2024-01-01 12:00:00.000", eventCurrency: "GBP" }
🔴DSR Constraint: AppsFlyer S2S cannot process DSR (data subject requests) on behalf of the device. DSR for AppsFlyer must go directly to AppsFlyer's Privacy Cloud API. This is an AppsFlyer platform limitation, not a CDP or sGTM limitation.

VendorsGTM TemplateTCF 2.0Dedup RequiredKey Identity SignalConsent Mode v2
Google AdsNativeNoNoGCLID / SHA256 Emailad_storage + ad_user_data
Meta CAPICommunityNo (own framework)YES — event_idfbp + fbc + SHA256 Emailad_storage
Amazon DSPNone — CustomREQUIREDNoAmazon Ad ID (opaque)Custom TC string param
TikTokCommunityNoYES — event_idttclid + _ttp + SHA256ad_storage
RedditNone — CustomNoYES — conversion_id_rdt_uuid + SHA256 Emailad_storage
moEngageNone — CustomNoNo (idempotent)unique_id (CRM ID)analytics_storage
AmplitudeCommunityNoYES — insert_iduser_id + device_idanalytics_storage
AppsFlyer S2SNone — CustomNoNoappsflyer_id (on-device)n/a (supplemental only)

CMP Configuration
& OneTrust Integration

Consent Management Platforms are the gatekeeper between user preference and data collection. In an sGTM architecture, consent must be correctly captured client-side and honoured both in the browser (blocking tags) and server-side (gating outbound vendor calls).

v2.2
TCF Standard
2
Consent Mode Versions
7
Key Consent Params
EU
GDPR Jurisdiction
UK
UK GDPR / PECR
User visits site gtag default state CMP BANNER Accept All Reject All Manage Preferences CONSENT STATE gtag('consent','update',...) ad_storage: granted analytics_storage: granted ad_user_data: granted ad_personalization: granted TCF string → cookie Web Container Tags fire per consent state Consent triggers gate sGTM Server Reads consent from event or TCF cookie in headers GRANTED PATH Full tracking + conversion all vendor tags fire Cookies set, IDs matched DENIED PATH Cookieless pings only Conversion modelling active GA4 models w/ ML No 3P vendor calls

01
Install OneTrust Script via GTM
Deploy OneTrust's script (CDN-hosted or self-hosted) via client-side GTM. It must load before any other scripts. Set it to fire on All Pages with the highest priority trigger. Use the "Consent Initialization" trigger in GTM, not "DOM Ready".
02
Configure Google Consent Mode v2
In OneTrust dashboard → Integrations → Google Consent Mode v2. Map OneTrust categories to consent parameters: "Targeting Cookies" → ad_storage + ad_user_data + ad_personalization. "Performance Cookies" → analytics_storage.
03
Set Default Denied State
In GTM, add a Consent Initialization tag that sets ALL consent types to denied before the CMP loads. This prevents any tag from firing before consent is obtained — required for GDPR compliance.
04
TCF 2.0 String Generation
OneTrust generates and stores the TCF 2.0 consent string in eupubconsent-v2 cookie. This is automatically updated when user changes consent preferences. sGTM reads this cookie from incoming request headers.
05
Geo-targeting Rules
Configure OneTrust geolocation rules: EU/EEA/UK → GDPR banner + opt-in (denied default). California → CCPA opt-out banner. Rest of world → implicit consent or no banner. OneTrust's CDN auto-detects IP for geo-routing.
// GTM — Consent Initialization Tag (fires first) window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } // Set denied defaults BEFORE OneTrust loads gtag('consent', 'default', { ad_storage: 'denied', analytics_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', functionality_storage: 'granted', security_storage: 'granted', wait_for_update: 2000 // ms to wait for CMP }); // OneTrust callback — updates consent function OptanonWrapper() { const groups = window.OnetrustActiveGroups; gtag('consent', 'update', { ad_storage: groups.includes('C0004') ? 'granted' : 'denied', analytics_storage: groups.includes('C0002') ? 'granted' : 'denied', ad_user_data: groups.includes('C0004') ? 'granted' : 'denied', ad_personalization: groups.includes('C0004') ? 'granted' : 'denied' }); } // OneTrust group IDs (default) // C0001 = Strictly Necessary // C0002 = Performance / Analytics // C0003 = Functional // C0004 = Targeting / Advertising
Re-consent on change: When a user changes consent, OneTrust fires OptanonWrapper again. GTM's Consent Mode v2 listener re-evaluates all pending tag executions. Tags with "Wait for update" will fire retroactively if consent is granted during the session.

Reading Consent in sGTM
sGTM receives the consent state forwarded from the web container. The GA4 client sends consent signals as part of the event payload. Use the x-ga-gcs parameter (Google Consent String) to read the compact consent representation.

Alternatively, read the eupubconsent-v2 cookie directly from the Cookie header of the incoming HTTP request for TCF-required vendors (Amazon DSP).
Server-Side Tag Gating
Each server tag should have a trigger condition that checks consent. Create a variable that reads x-ga-gcs or parses the event model's consent object. Use this as a trigger exception — if ad_storage is denied, block the advertising vendor tags from firing.

This provides a true second layer of consent enforcement, independent of the browser.
// sGTM Custom Variable — Parse Consent State from event const getEventData = require('getEventData'); // GA4 sends consent state via x-ga-gcs header // Format: G100 = analytics denied; G111 = all granted const consentStr = getEventData('x-ga-gcs'); // Or read from event model consent object const adStorage = getEventData('consent.ad_storage'); const analyticsStorage = getEventData('consent.analytics_storage'); const adUserData = getEventData('consent.ad_user_data'); return { adGranted: adStorage === 'granted', analyticsGranted: analyticsStorage === 'granted', userDataGranted: adUserData === 'granted' };

GCP Microservice
Consent Platform

For enterprises requiring full control over consent data, custom audit trails, or multi-jurisdiction logic too complex for commercial CMPs, a GCP-native microservice CMP is the gold standard. This gives you a consent graph stored in your own cloud, versioned, queryable, and integrated directly with sGTM.

When to build custom: Custom CMP makes sense when you need: (1) consent stored in your own BigQuery for DSR query, (2) consent decisions embedded in your CDP/data lake event stream, (3) multi-brand consent federation, (4) real-time consent propagation to 30+ downstream systems, or (5) consent API for backend services (not just browser).
BROWSER Custom CMP UI Component gtag consent bridge CONSENT API Cloud Run — Python Flask POST /consent/record GET /consent/status/{uid} POST /consent/withdraw TCF string builder GeoIP + Jurisdiction Logic CLOUD PUB/SUB consent-events topic Real-time propagation Fan-out to subscribers FIRESTORE Real-time consent store consent/{uid}/versions Immutable audit log TTL: 6 years (GDPR) BIGQUERY consent_events table DSR queries <30 days Consent analytics Jurisdiction reporting sGTM LOOKUP HOOK Cloud Function sGTM enrichment variable reads Firestore consent returns consent object DSR SERVICE Cloud Run — Python POST /dsr/delete → purge GET /dsr/export → DSAR 30-day SLA tracker DOWNSTREAM sGTM Container Meta CAPI Google Ads moEngage Amplitude AppsFlyer BigQuery CDP Reddit CAPI

from flask import Flask, request, jsonify from google.cloud import firestore, pubsub_v1 import hashlib, datetime, json app = Flask(__name__) db = firestore.Client() publisher = pubsub_v1.PublisherClient() TOPIC = "projects/ai-fix-pro/topics/consent-events" @app.route('/consent/record', methods=['POST']) def record_consent(): data = request.get_json() uid = data['user_id'] consent = { 'uid': uid, 'ad_storage': data.get('ad_storage'), 'analytics_storage': data.get('analytics_storage'), 'ad_user_data': data.get('ad_user_data'), 'tcf_string': data.get('tcf_string'), 'jurisdiction': data.get('jurisdiction', 'EU'), 'timestamp': datetime.datetime.utcnow().isoformat(), 'version': '2.0', 'ip_hash': hashlib.sha256( request.remote_addr.encode()).hexdigest() } # Store in Firestore (immutable append) ref = db.collection('consent').document(uid) ref.collection('versions').add(consent) ref.set({'latest': consent}, merge=True) # Publish to downstream systems publisher.publish(TOPIC, json.dumps(consent).encode()) return jsonify({'status': 'ok', 'uid': uid}), 200 @app.route('/consent/status/<uid>') def get_status(uid): doc = db.collection('consent').document(uid).get() if doc.exists: return jsonify(doc.to_dict()['latest']) return jsonify({'status': 'no_record'}), 404
// sGTM Custom Variable — calls GCP Consent API const sendHttpGet = require('sendHttpGet'); const getEventData = require('getEventData'); const JSON = require('JSON'); const returnValue = require('returnValue'); const runAsyncTask = require('runAsyncTask'); const userId = getEventData('user_id'); if (!userId) { returnValue(null); return; } runAsyncTask(() => { sendHttpGet( `https://consent.yourdomain.com/consent/status/${userId}`, (statusCode, headers, body) => { if (statusCode === 200) { const consent = JSON.parse(body); returnValue({ ad_granted: consent.ad_storage === 'granted', analytics_granted: consent.analytics_storage === 'granted', tcf_string: consent.tcf_string, jurisdiction: consent.jurisdiction }); } else { returnValue({ ad_granted: false, analytics_granted: false }); } } ); });
Real-time consent graph: This pattern means every single sGTM event is verified against your authoritative consent store before forwarding to any vendor. A consent withdrawal propagates within seconds across all channels.

Right to Erasure
POST /dsr/delete → triggers deletion across: Firestore user record, GA4 User Deletion API, Meta Data Deletion API (72h), AppsFlyer Deletion API, BigQuery GDPR_DELETE (DML). SLA: 30 days. Track via Cloud Tasks queue with retry.
Right to Access (DSAR)
GET /dsr/export → queries BigQuery for all events linked to user_id, Firestore for consent history, returns structured JSON. Must include all derived data. Cloud Run service with PDF generation via ReportLab. Max 30-day SLA.
Consent Audit Trail
Every consent action appended to Firestore subcollection with immutable timestamp, IP hash, version, jurisdiction. BigQuery sink for analytics. 6-year retention minimum (GDPR accountability principle). DPA-ready exports available on demand.

Limitations,
Gotchas & Gaps

sGTM is powerful but not a silver bullet. Understanding where it breaks down — and why — is essential to designing a robust enterprise implementation. Below is an exhaustive reference of known limitations organised by category.

18+
Known Gotchas
5
Vendor-Specific Limits
3
Privacy Law Constraints
2
Platform Hard Limits
⚠ JavaScript Sandbox Restrictions
sGTM Custom Templates run in a sandboxed JS environment. Not all Node.js or browser APIs are available.
  • No fetch() — use sendHttpRequest() API
  • No setTimeout — use runAsyncTask()
  • No access to process, fs, or Node globals
  • No npm packages — only approved Sandboxed JS APIs
  • Template permissions model — must declare all external URL calls in template metadata
⚠ Cookie Limitations
sGTM can set server-side cookies (via Set-Cookie response headers), but:
  • Only works when the sGTM endpoint is on a first-party subdomain of your site
  • Cookies set via sGTM cannot access HttpOnly cookies set by your app backend on the same subdomain (separate origin)
  • Safari still partitions cookies by domain — first-party status must be maintained
  • iOS 17+ private relay hides IP, breaking geo-IP-based logic
  • Chrome CHIPS partitioned cookies: cross-site iframe scenarios become complex
🔴 Amazon DSP — TCF 2.0 Hard Requirement
Amazon DSP will silently drop conversion events that lack a valid TCF 2.0 consent string in GDPR regions. No error returned — events simply aren't recorded.
  • Must pass gdpr=1 and gdpr_consent={{TC_STRING}}
  • TC string must encode Purposes 1, 3, 4, and Legitimate Interest claims
  • String must be generated by a registered CMP (IAB registered list)
  • OneTrust, Cookiebot, Quantcast all qualify
  • Custom CMPs must register with IAB Europe CMP list
🔴 AppsFlyer SDK Cannot Be Replaced
sGTM cannot replicate on-device AppsFlyer SDK functionality:
  • SKAN (SKAdNetwork) requires native iOS SDK integration
  • Google Play Referrer requires Android SDK
  • Deep link attribution requires SDK
  • AppsFlyer kit wrapper (mParticle/Segment) needs SDK on device
  • S2S API only supplements — cannot attribute installs server-side
  • DSR must go direct to AppsFlyer Privacy Cloud (not via sGTM)
⚠ Cold Start Latency
Cloud Run scales to zero by default. On the first request after idle period, container needs to boot (typically 2-5 seconds). During this time, events are queued and processed but with delayed response.
  • Solution: Set --min-instances=1 in Cloud Run config
  • Cost: ~$18/month per min instance (europe-west2, 512Mi)
  • For >100k events/day: consider min-instances=2 for redundancy
  • Preview server: separate instance, also keep warm
⚠ Meta CAPI Deduplication Failure
The most common production issue with sGTM Meta CAPI:
  • If browser pixel AND server tag both fire, events are counted twice
  • Must pass identical event_id from both sources
  • Generate event_id client-side, push to dataLayer, forward via GA4 event
  • Dedup window is 48h — late S2S events outside window are NOT deduped
  • Event name must also match exactly (case-sensitive)
⚠ No Native Segment/mParticle Client
sGTM does not have native clients for CDP platforms:
  • Segment Analytics.js events cannot be natively parsed without custom client
  • Must build custom client to parse Segment Track/Page/Identify payloads
  • mParticle server-to-server events require custom HTTP client in sGTM
  • Workaround: use Segment Functions or mParticle Forwarding Rules as parallel path
⚠ Real-Time Debug Complexity
Debugging sGTM is significantly harder than client-side GTM:
  • Must activate both web container AND server container preview simultaneously
  • Preview URL requires ?gtm_debug=x parameter AND server preview token
  • Cloud Run logs (stderr) show errors but with up to 30s delay in Log Explorer
  • Third-party tag errors appear as HTTP response codes only — no JS stack traces
  • Solution: build a local sGTM mock environment using Docker for dev testing
🔴 Privacy Laws — Consent Propagation Delay
A legally significant gap: consent withdrawal must be immediately effective under GDPR Article 7(3). But:
  • sGTM reads consent state from event payload — if browser sends cached consent, server fires
  • Custom GCP CMP with real-time Firestore lookup closes this gap (see Tab 04)
  • Commercial CMPs rely on cookie state — up to 30-min lag on withdrawal propagation
  • Backend data pipelines (BigQuery) may process events before withdrawal is applied
  • Best practice: apply consent filtering at query time as well as collection time
⚠ IP Address Geolocation
sGTM receives the client IP in the HTTP request, but:
  • If your site uses a CDN (Cloudflare, Fastly), the IP may be the CDN edge IP
  • Must configure CDN to pass X-Forwarded-For or X-Real-IP headers
  • sGTM reads x-forwarded-for header — use sGTM variable to extract original IP
  • iOS 17+ Private Relay: Apple proxy IP replaces real IP → geo-detection breaks
  • MaxMind GeoIP2 or Google's IP geolocation API required for city-level accuracy
⚠ Google Analytics Data Thresholds
GA4 via sGTM is subject to the same platform limitations:
  • Data thresholds applied to reports when user counts are low (<50 in some segments)
  • Exploration reports: max 10M events per query
  • Realtime report: up to 30 min delay for server-side events
  • BigQuery export: up to 24h delay; not suitable for real-time dashboards
  • Custom dimensions capped at 50 event-scoped, 25 user-scoped
⚠ Template Community Trust Model
Many vendor tags rely on the GTM Template Gallery (community-maintained):
  • Community templates are NOT reviewed by Google for security
  • A compromised community template could exfiltrate data to attacker servers
  • Best practice: fork community templates, audit the code, host internally
  • Block external URL calls in template permissions to only your approved domains
  • Review template code on every update before deploying to production

VendorTCF 2.0 RequiredConsent FrameworkDenied Mode BehaviourData Residency Option
Google AdsNot requiredConsent Mode v2Cookieless pings + conversion modellingEU data processing terms
GA4Not requiredConsent Mode v2Cookieless pings, ML modellingEU-only data region
Meta CAPIRecommended for EUMeta's own + LDULimited Data Use (LDU) flagEU data processing
Amazon DSPMANDATORY in EUIAB TCF 2.0Event silently droppedEU hosted (check contract)
TikTokRecommendedOwn privacy policyLimited data mode flagEU/US data centres
RedditCheck DPAOwn privacy policyNo events sentUS only currently
moEngageNot requiredGDPR processorDo not send eventsEU data centre (api-eu.)
AmplitudeNot requiredGDPR processorDo not send eventsEU isolation flag
AppsFlyerFor DSR processingPrivacy CloudS2S events not sentEU data residency

✓ Do use sGTM for
• Web conversion events (purchase, lead, signup)
• Server-authoritative event confirmation
• Cookie lifetime extension to 400 days
• Real-time data enrichment from CRM
• Reducing browser payload (remove 3P scripts)
• First-party cookie management
• Multi-vendor fan-out from single event
• Consent-gated vendor forwarding
⚠ Consider carefully
• Mobile attribution — SDK must stay on-device
• Real-time personalisation — propagation latency
• High-frequency events (>10M/day) — cost scales
• Consent withdrawal — needs custom CMP lookup
• Amazon DSP — TCF string complexity
• Community templates — security audit required
• CDN IP masking — geolocation accuracy
• Multi-region latency — deploy per region
✗ Not designed for
• Mobile MMP attribution (replace AppsFlyer SDK)
• Real-time audience building (use CDP instead)
• Replacing full CDPs (mParticle/Segment)
• Server-side rendering personalisation
• Email / offline touchpoint attribution
• Sub-100ms latency requirements
• Complex ML model inference
• Long-running background jobs

Top 5 Ecommerce Events
Step-by-Step Integration

The five most commercially critical ecommerce events — view_item, add_to_cart, begin_checkout, purchase, and refund — each have distinct field requirements, dedup strategies, and downstream configurations per vendor. This tab walks through each event end-to-end: dataLayer push → sGTM variable mapping → vendor-specific payload for all 8 platforms.

Architecture principle: One GA4 event hits sGTM. sGTM fans it out to all vendors simultaneously. Each vendor tag reads from the same event model but maps fields to its own schema. The dataLayer structure below follows GA4 Enhanced Ecommerce spec — the universal source of truth for all downstream mappings.
view_item
TRIGGER: User lands on a product detail page (PDP)
COMMERCIAL VALUE: Top-of-funnel signal — feeds retargeting audiences & catalogue ads
STEP 1 dataLayer Push — Client Side
Push the GA4 Enhanced Ecommerce view_item event from your frontend. This is the single source of truth that feeds all downstream vendor mappings via sGTM.
// Fire on product detail page load window.dataLayer.push({ ecommerce: null }); // clear previous window.dataLayer.push({ event: 'view_item', event_id: 'vi_' + Date.now() + '_' + Math.random().toString(36).substr(2,9), user_id: {{CRM_USER_ID_IF_LOGGED_IN}}, // optional ecommerce: { currency: 'GBP', value: 49.99, items: [{ item_id: 'SKU-001', item_name: 'Premium Running Shoes', item_brand: 'NikeX', item_category: 'Footwear', item_category2: 'Running', item_variant: 'Blue/42', price: 49.99, quantity: 1, index: 0 }] } });
STEP 2 sGTM — Variable Setup
Create these variables in your sGTM server container. They read from the GA4 event model and are referenced by all vendor tags.
EC — Currency
ecommerce.currency
EC — Value
ecommerce.value
EC — Items Array
ecommerce.items
EC — Item ID [0]
ecommerce.items.0.item_id
EC — Item Name [0]
ecommerce.items.0.item_name
EC — Item Category
ecommerce.items.0.item_category
EC — Item Price [0]
ecommerce.items.0.price
Event ID
event_id
User ID
user_id
STEP 3 Vendor Tag Configurations
Google AdsRemarketing
Tag TypeGoogle Ads Remarketing (server tag)
Event SnippetMap to view_item conversion action OR standard remarketing event
ecomm_prodid{{EC — Item ID [0]}}
ecomm_pagetype'product' (hardcoded)
ecomm_totalvalue{{EC — Value}}
TriggerEvent Name equals view_item AND ad_storage = granted
💡 Use Google Ads Dynamic Remarketing tag to feed the product feed-based retargeting. Pass item_id matching your Google Merchant Centre feed.
Meta CAPIViewContent
event_name"ViewContent"
event_id{{Event ID}} — MUST match browser pixel
content_ids["{{EC — Item ID [0]}}"]
content_type"product"
value{{EC — Value}}
currency{{EC — Currency}}
⚠ Dedup: fire ViewContent from browser pixel AND server tag with same event_id. Meta will keep only one within 48h.
Amazon DSPProductView
event paramevent=productView in pixel URL
pidYour Amazon DSP Advertiser Tag ID
gdpr_consent{{TCF String Variable}} — MANDATORY
Product signalPass item_id as custom param if using AMC attribution
🔴 No TCF string = event silently dropped. Ensure eupubconsent-v2 cookie is readable in sGTM request headers.
TikTokViewContent
event"ViewContent"
event_id{{Event ID}}
content_id{{EC — Item ID [0]}}
content_type"product"
value{{EC — Value}}
currency"GBP"
💡 Pass _ttp cookie for TikTok click attribution stitching. ViewContent feeds TikTok's product catalogue retargeting.
RedditViewContent
tracking_type"ViewContent"
products[].id{{EC — Item ID [0]}}
products[].name{{EC — Item Name [0]}}
products[].category{{EC — Item Category}}
value{{EC — Value}}
💡 Reddit ViewContent events feed interest-targeting signals. Less critical than purchase but still builds audience segments.
moEngageproduct_viewed
action"product_viewed"
attributes.product_id{{EC — Item ID [0]}}
attributes.product_name{{EC — Item Name [0]}}
attributes.price{{EC — Item Price [0]}}
attributes.category{{EC — Item Category}}
💡 This event feeds moEngage Browse Abandonment journeys. Only send if analytics_storage = granted.
AmplitudeProduct Viewed
event_type"Product Viewed"
insert_id{{Event ID}} — prevents duplicates
event_properties.product_id{{EC — Item ID [0]}}
event_properties.price{{EC — Item Price [0]}}
event_properties.category{{EC — Item Category}}
💡 Used in Amplitude funnel analysis. Feeds "Product Viewed → Purchased" conversion funnel charts.
AppsFlyer S2Saf_content_view
eventName"af_content_view"
af_content_id{{EC — Item ID [0]}}
af_content_type"product"
af_price{{EC — Item Price [0]}}
af_currency"GBP"
⚠ Only send if appsflyer_id is available from backend. Web-only sessions won't have an AF device ID.
add_to_cart
TRIGGER: User clicks "Add to Basket" / "Add to Cart" button
COMMERCIAL VALUE: High-intent signal — cart abandonment retargeting, bid optimisation
STEP 1 dataLayer Push — Client Side
// Fire on Add to Cart button click (or after AJAX response confirms cart add) window.dataLayer.push({ ecommerce: null }); window.dataLayer.push({ event: 'add_to_cart', event_id: 'atc_' + Date.now() + '_' + Math.random().toString(36).substr(2,9), user_id: {{CRM_USER_ID}}, ecommerce: { currency: 'GBP', value: 49.99, // price × quantity of added item(s) items: [{ item_id: 'SKU-001', item_name: 'Premium Running Shoes', item_brand: 'NikeX', item_category: 'Footwear', item_variant: 'Blue/42', price: 49.99, quantity: 1 // quantity added in this action }] } });
Timing note: Fire AFTER the cart add is confirmed (AJAX response 200), not on button click. Firing on click before confirmation inflates add_to_cart counts with failed cart adds.
STEP 2 sGTM — Variable Setup
Same variables as view_item plus quantity. Create an additional variable for the added quantity.
EC — Quantity [0]
ecommerce.items.0.quantity
EC — Cart Value
ecommerce.value
EC — Item Variant
ecommerce.items.0.item_variant
STEP 3 Vendor Tag Configurations
Google AdsAddToCart
Tag TypeGoogle Ads Remarketing
ecomm_prodid{{EC — Item ID [0]}}
ecomm_pagetype'cart'
ecomm_totalvalue{{EC — Cart Value}}
Conversion ActionOptional: fire Add-to-Cart micro-conversion if tracking in Smart Bidding
💡 add_to_cart as a Google Ads conversion is valuable for PMAX campaigns — it feeds the value-based bidding signal ladder.
Meta CAPIAddToCart
event_name"AddToCart"
event_id{{Event ID}} — dedup key
content_ids["{{EC — Item ID [0]}}"]
content_type"product"
value{{EC — Cart Value}}
currency"GBP"
💡 AddToCart is Meta's strongest retargeting signal for catalogue ads and Dynamic Ads. Highest priority after Purchase for ROAS campaigns.
Amazon DSPaddToCart
event paramevent=addToCart
gdpr_consent{{TCF String Variable}}
v (value){{EC — Cart Value}}
🔴 TCF consent string required. Amazon uses this signal to power cart-abandonment DSP audience segments.
TikTokAddToCart
event"AddToCart"
content_id{{EC — Item ID [0]}}
content_type"product"
value{{EC — Cart Value}}
currency"GBP"
💡 AddToCart feeds TikTok's Shopping campaign optimisation. Needed for Value-Based Bidding (VBB) on TikTok Ads.
RedditAddToCart
tracking_type"AddToCart"
products[].id{{EC — Item ID [0]}}
value{{EC — Cart Value}}
currency"GBP"
conversion_id{{Event ID}}
💡 Reddit's attribution model benefits from mid-funnel signals. AddToCart improves Reddit Ads optimisation for ecommerce campaigns.
moEngageadd_to_cart
action"add_to_cart"
attributes.product_id{{EC — Item ID [0]}}
attributes.product_name{{EC — Item Name [0]}}
attributes.quantity{{EC — Quantity [0]}}
attributes.price{{EC — Item Price [0]}}
💡 This is the primary trigger for moEngage Cart Abandonment automation journeys. High business value — configure within 1h abandonment window.
AmplitudeProduct Added
event_type"Product Added"
insert_id{{Event ID}}
event_properties.product_id{{EC — Item ID [0]}}
event_properties.quantity{{EC — Quantity [0]}}
event_properties.price{{EC — Item Price [0]}}
💡 Amplitude "Product Added → Order Completed" funnel is a standard ecommerce chart. Critical for checkout funnel drop-off analysis.
AppsFlyer S2Saf_add_to_cart
eventName"af_add_to_cart"
af_content_id{{EC — Item ID [0]}}
af_price{{EC — Item Price [0]}}
af_quantity{{EC — Quantity [0]}}
af_currency"GBP"
⚠ Only fire if appsflyer_id available from your backend session store. This event feeds AF's retargeting audience for web-to-app campaigns.
begin_checkout
TRIGGER: User reaches checkout page / initiates checkout flow
COMMERCIAL VALUE: Strongest pre-purchase signal — checkout abandonment retargeting
STEP 1 dataLayer Push — Client Side
// Fire when checkout page loads or user clicks "Proceed to Checkout" window.dataLayer.push({ ecommerce: null }); window.dataLayer.push({ event: 'begin_checkout', event_id: 'bco_' + Date.now() + '_' + Math.random().toString(36).substr(2,9), user_id: {{CRM_USER_ID}}, ecommerce: { currency: 'GBP', value: 89.98, // total basket value at checkout coupon: 'SUMMER10', // if applied items: [ // All items in basket at checkout time { item_id: 'SKU-001', item_name: 'Premium Running Shoes', item_brand: 'NikeX', item_category: 'Footwear', price: 49.99, quantity: 1 }, { item_id: 'SKU-045', item_name: 'Sports Socks Pack', item_brand: 'NikeX', item_category: 'Accessories', price: 39.99, quantity: 1 } ] } });
Multi-item basket: Pass the full basket contents at begin_checkout — not just the last added item. This is required for Meta Dynamic Ads and TikTok Shopping to show the correct products in cart-abandonment ads.
STEP 2 sGTM — Additional Variables for Multi-Item
EC — Items JSON
Custom JS: JSON.stringify(ecommerce.items)
EC — Content IDs
Custom JS: extract array of item_ids
EC — Coupon
ecommerce.coupon
EC — Num Items
Custom JS: ecommerce.items.length
EC — Basket Value
ecommerce.value
EC — Currency
ecommerce.currency
// sGTM Custom Variable: Extract content_ids array (for Meta / TikTok) const getEventData = require('getEventData'); const items = getEventData('ecommerce.items') || []; return items.map(i => i.item_id);
STEP 3 Vendor Tag Configurations
Google AdsCheckout
Tag TypeRemarketing + optional Conversion
ecomm_prodid{{EC — Content IDs}} (array)
ecomm_pagetype'checkout'
ecomm_totalvalue{{EC — Basket Value}}
💡 Checkout step triggers Google's "Checkout Abandonment" audience list. Add to RLSA bid modifier campaigns for high-value checkout re-engagement.
Meta CAPIInitiateCheckout
event_name"InitiateCheckout"
event_id{{Event ID}}
content_ids{{EC — Content IDs}} (full array)
num_items{{EC — Num Items}}
value{{EC — Basket Value}}
currency"GBP"
💡 InitiateCheckout is Meta's highest-value retargeting signal below Purchase. Used in "Checkout Started but not Purchased" custom audience segments.
Amazon DSPcheckout
event paramevent=checkout
v{{EC — Basket Value}}
gdpr_consent{{TCF String Variable}}
🔴 TCF string mandatory. Amazon uses begin_checkout as the anchor event for checkout-abandonment DSP retargeting sequences.
TikTokInitiateCheckout
event"InitiateCheckout"
contentsArray: [{content_id, content_type, quantity, price}]
value{{EC — Basket Value}}
currency"GBP"
💡 TikTok's checkout event feeds ROAS-optimised "Purchase" campaigns and the checkout abandonment retargeting audience (1-30 day windows).
RedditLead (proxy)
tracking_type"Lead" (Reddit has no CheckoutStart event)
value{{EC — Basket Value}}
currency"GBP"
⚠ Reddit does not have a native begin_checkout event type. Use custom_event_name: "checkout_started" alongside Lead tracking_type. Reddit Ads team recommends this mapping.
moEngagecheckout_started
action"checkout_started"
attributes.basket_value{{EC — Basket Value}}
attributes.item_count{{EC — Num Items}}
attributes.product_ids{{EC — Content IDs}}
💡 This event triggers the highest-priority moEngage abandonment journey: checkout-abandonment email/push within 30 minutes. Conversion rate impact is typically 5-15%.
AmplitudeCheckout Started
event_type"Checkout Started"
insert_id{{Event ID}}
event_properties.revenue{{EC — Basket Value}}
event_properties.products{{EC — Items JSON}}
💡 Amplitude uses this as step 3 of a standard 4-step ecommerce funnel (View → Cart → Checkout → Purchase). Feeds abandonment rate cohort analysis.
AppsFlyer S2Saf_initiated_checkout
eventName"af_initiated_checkout"
af_price{{EC — Basket Value}}
af_content_list{{EC — Content IDs}} (JSON array)
af_quantity{{EC — Num Items}}
⚠ Only relevant if cross-device web-to-app checkout flow exists. AppsFlyer will stitch this to the app session if appsflyer_id matches.
purchase
TRIGGER: Order confirmed — fire ONLY once, server-authoritative
COMMERCIAL VALUE: The primary conversion event — revenue, ROAS, attribution
STEP 1 dataLayer Push — Server-Authoritative Pattern
🔴Critical architecture decision: Never fire purchase from the thank-you page JavaScript alone — it double-fires on refresh and misses users with JS disabled. The canonical pattern: fire from your order confirmation backend (Measurement Protocol direct to sGTM) OR use transaction_id deduplication in GA4. Always use server-side confirmation as the source of truth.
// Pattern A: Standard dataLayer push on thank-you page // (use transaction_id dedup in GA4 to handle page refreshes) window.dataLayer.push({ ecommerce: null }); window.dataLayer.push({ event: 'purchase', event_id: 'pur_' + '{{ORDER_ID}}', // stable, order-based ID user_id: '{{CRM_USER_ID}}', ecommerce: { transaction_id: 'ORDER-20240101-9876', // GA4 dedup key value: 89.98, tax: 15.00, shipping: 4.99, currency: 'GBP', coupon: 'SUMMER10', affiliation: 'Web Store', items: [{ item_id: 'SKU-001', item_name: 'Premium Running Shoes', item_brand: 'NikeX', item_category: 'Footwear', price: 49.99, quantity: 1 }, { item_id: 'SKU-045', item_name: 'Sports Socks Pack', price: 39.99, quantity: 1 }] } }); // Pattern B: Backend Measurement Protocol → sGTM (preferred for high integrity) // Python backend fires directly to sGTM endpoint after payment confirmed // import requests // requests.post('https://gtm.yourdomain.com/g/collect', json={...})
STEP 2 sGTM Variables — Purchase-Specific
EC — Transaction ID
ecommerce.transaction_id
EC — Revenue
ecommerce.value
EC — Tax
ecommerce.tax
EC — Shipping
ecommerce.shipping
EC — Coupon
ecommerce.coupon
EC — Affiliation
ecommerce.affiliation
STEP 3 Vendor Tag Configurations — Purchase
Google AdsPurchase Conversion
Tag TypeGoogle Ads Conversion Tracking
Conversion ID / LabelAW-XXXXXXX / purchase-label
value{{EC — Revenue}} (ex. tax, ex. shipping)
currency"GBP"
transaction_id{{EC — Transaction ID}} — GA dedup
Enhanced Conv.sha256_email + sha256_phone + sha256_first_name
💡 Set value to revenue only (excluding tax + shipping) for accurate ROAS. Google Ads uses this for Smart Bidding value optimisation.
Meta CAPIPurchase
event_name"Purchase"
event_idOrder-based ID: "pur_ORDER-ID"
content_ids{{EC — Content IDs}}
content_type"product"
value{{EC — Revenue}}
currency"GBP"
order_id{{EC — Transaction ID}}
⚠ Pass all 7 user data fields (em, ph, fn, ln, ct, zp, country) for maximum EMQ score. Target EMQ >8.0 for purchase events. Each extra field adds ~1-2% match rate.
Amazon DSPpurchase
event paramevent=purchase
v (order value){{EC — Revenue}}
oid{{EC — Transaction ID}}
gdpr_consent{{TCF String Variable}}
🔴 TCF string mandatory. Amazon purchase event feeds DSP exclusion audiences (exclude purchasers from acquisition campaigns) and AMC attribution reporting.
TikTokCompletePayment
event"CompletePayment"
event_id{{EC — Transaction ID}} — use order ID
contentsArray of {content_id, content_type, quantity, price}
value{{EC — Revenue}}
currency"GBP"
💡 TikTok calls purchase "CompletePayment". Use transaction_id as event_id for natural deduplication. This feeds TikTok's ROAS bidding and lookalike purchaser audiences.
RedditPurchase
tracking_type"Purchase"
products[].idFull items array item_ids
value{{EC — Revenue}}
currency"GBP"
conversion_id{{EC — Transaction ID}}
💡 Reddit purchase feeds lookalike audiences ("Purchasers") and CPA campaign optimisation. Reddit attribution window: 1-day click, 28-day view.
moEngageorder_completed
action"order_completed"
attributes.order_id{{EC — Transaction ID}}
attributes.revenue{{EC — Revenue}}
attributes.coupon_code{{EC — Coupon}}
attributes.product_ids{{EC — Content IDs}}
💡 Triggers post-purchase journeys: order confirmation email, upsell sequences (30d), winback prevention, loyalty points. Update user LTV attribute simultaneously.
AmplitudeOrder Completed
event_type"Order Completed"
insert_id{{EC — Transaction ID}} — natural dedup
event_properties.revenue{{EC — Revenue}}
event_properties.order_id{{EC — Transaction ID}}
event_properties.products{{EC — Items JSON}}
$revenue (special){{EC — Revenue}} — feeds Revenue chart
💡 Pass $revenue (dollar sign prefix) to populate Amplitude's Revenue chart automatically. Also set user_properties.$set: {ltv: updated_ltv} to update lifetime value.
AppsFlyer S2Saf_purchase
eventName"af_purchase"
af_revenue{{EC — Revenue}}
af_currency"GBP"
af_order_id{{EC — Transaction ID}}
af_content_list{{EC — Content IDs}}
💡 S2S purchase is the gold standard for AppsFlyer LTV calculation. Pass af_revenue to feed Cohort LTV charts. This requires appsflyer_id from your backend user profile.
refund
TRIGGER: Order refunded — always fire from backend, never from browser
COMMERCIAL VALUE: Revenue correction, exclusion lists, net ROAS calculation
STEP 1 dataLayer / Measurement Protocol Push — Backend Only
🔴Refunds must be server-authoritative. A refund is a backend financial event — it NEVER has a browser context. Fire exclusively via Measurement Protocol from your OMS/ERP/payments backend directly to your sGTM endpoint or GA4 MP endpoint.
# Python backend — fire refund to sGTM via Measurement Protocol import requests payload = { "client_id": "{{CRM_USER_ID}}", "user_id": "{{CRM_USER_ID}}", "events": [{ "name": "refund", "params": { "currency": "GBP", "value": -49.99, # negative value "transaction_id": "ORDER-20240101-9876", "refund_amount": 49.99, "items": [{ "item_id": "SKU-001", "item_name": "Premium Running Shoes", "price": 49.99, "quantity": 1 }] } }] } # POST to your sGTM endpoint (which routes to all vendors) r = requests.post( "https://gtm.yourdomain.com/g/collect", params={"measurement_id": "G-XXXXXXXX", "api_secret": "YOUR_MP_SECRET"}, json=payload )
STEP 3 Vendor Tag Configurations — Refund
Google AdsNo native refund
ApproachGoogle Ads has no refund event. Use conversion adjustment API via Google Ads API to retract/adjust the original purchase conversion.
Conversion AdjustmentUpload via Google Ads API: ConversionAdjustmentUploadService with adjustment_type RETRACTION
⚠ Cannot retract via sGTM. Must use Google Ads API upload. Do this to prevent inflated ROAS from refunded orders corrupting Smart Bidding models.
Meta CAPINo native refund
ApproachMeta has no refund event type. Strategy: send a custom event "Refund" or suppress original purchase from ROAS reports via offline event set correction.
Custom Eventevent_name: "Refund", value: negative, order_id for correlation
⚠ For refund-heavy verticals (>10% rate), this is a known gap in Meta's CAPI. Workaround: use net revenue (after refunds) in your purchase value reporting.
Amazon DSPNo refund support
ApproachAmazon DSP pixel has no refund event. Refunds cannot be retroactively applied via sGTM. Contact Amazon DSP account team for attribution correction via AMC data upload.
🔴 Known limitation. Use AMC (Amazon Marketing Cloud) SQL to exclude refunded orders from ROAS calculations post-hoc.
TikTokNo native refund
ApproachTikTok Events API has no refund event. Use Custom Event with event: "Refund" for tracking but this won't auto-deduct from ROAS metrics in TikTok Ads Manager.
⚠ Pass negative revenue as custom event property for internal analytics correlation. TikTok attribution reports must be manually adjusted for refund rates.
RedditNo refund event
ApproachReddit Conversions API has no refund event type. Refunded customers can be added to a custom audience exclusion list via CSV upload in Reddit Ads.
💡 Send custom event "Refund" for your own analytics. Exclude hashed emails of refunders from Reddit retargeting audiences manually.
moEngageorder_refunded ✓
action"order_refunded" — fully supported
attributes.order_id{{EC — Transaction ID}}
attributes.refund_amountRefunded value
attributes.reasonRefund reason code from OMS
✓ moEngage natively supports refund events. Triggers: refund confirmation email/SMS, winback journey initiation, churn risk segmentation, and LTV recalculation.
AmplitudeOrder Refunded ✓
event_type"Order Refunded"
insert_id"ref_" + transaction_id
$revenueNegative value: -49.99
$revenueType"Refund"
event_properties.order_id{{EC — Transaction ID}}
✓ Amplitude natively supports negative $revenue. This auto-adjusts the Revenue chart and LTV calculations. Pass $revenueType: "Refund" to separate from Purchase revenue in charts.
AppsFlyer S2Saf_purchase (negative)
ApproachNo dedicated refund event. Send af_purchase with negative af_revenue value to correct cohort LTV.
af_revenueNegative: -49.99
af_order_idOriginal order ID with _refund suffix
💡 AppsFlyer's recommended approach for refund correction. The negative revenue value adjusts the cohort LTV charts in the AppsFlyer dashboard.

Vendor view_item add_to_cart begin_checkout purchase refund
Google AdsRemarketing: productRemarketing: cartRemarketing: checkoutConversion TagAPI only
Meta CAPIViewContentAddToCartInitiateCheckoutPurchaseCustom event
Amazon DSPproductViewaddToCartcheckoutpurchaseNot supported
TikTokViewContentAddToCartInitiateCheckoutCompletePaymentCustom event
RedditViewContentAddToCartLead (proxy)PurchaseNot supported
moEngageproduct_viewedadd_to_cartcheckout_startedorder_completedorder_refunded ✓
AmplitudeProduct ViewedProduct AddedCheckout StartedOrder CompletedOrder Refunded ✓
AppsFlyer S2Saf_content_viewaf_add_to_cartaf_initiated_checkoutaf_purchaseaf_purchase (neg)