0% found this document useful (0 votes)
4 views27 pages

SCSTrade Developer Guide v2

The SCSTrade Developer Implementation Guide provides a comprehensive setup for a Tracking & Attribution System on a self-hosted server, detailing access credentials, server setup, website code, CRM integration, and testing. It emphasizes the importance of various credentials for connecting multiple platforms like GTM, Meta, and Google Ads, and outlines the technical steps for deploying a GTM Server Container on a VPS. The guide also includes instructions for implementing website tracking code and ensuring data accuracy through server-side tracking.

Uploaded by

ranaammar2255x
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views27 pages

SCSTrade Developer Guide v2

The SCSTrade Developer Implementation Guide provides a comprehensive setup for a Tracking & Attribution System on a self-hosted server, detailing access credentials, server setup, website code, CRM integration, and testing. It emphasizes the importance of various credentials for connecting multiple platforms like GTM, Meta, and Google Ads, and outlines the technical steps for deploying a GTM Server Container on a VPS. The guide also includes instructions for implementing website tracking code and ensuring data accuracy through server-side tracking.

Uploaded by

ranaammar2255x
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

SCSTrade

Developer Implementation Guide


Tracking & Attribution System — Complete Setup

Own Server · No Google Cloud · Full Attribution Stack

Section What It Covers


Part 1 — Access & Credentials Every login, token, and key you need — why and where
Part 2 — Server Setup Deploy GTM Server on your own VPS/server
Part 3 — Website Code Tracking ID, UTM capture, form events, CTA listeners
Part 4 — CRM Integration Database schema, lead API, pipeline stages
Part 5 — Event System Stage-change events, SHA-256 hashing, Server GTM relay
Part 6 — Trading Platform First deposit and first trade event hooks
Part 7 — Google Ads Offline GCLID upload to close the attribution loop
Part 8 — Testing Checklist Verify every layer before going live

SCSTrade Internal — Developer Reference v2.0


PART 1 — ACCESS & CREDENTIALS
Every account, login, token, and key the developer needs — with full context

This section is not just a list of things to collect. It explains what each credential IS, why the system
cannot function without it, and what will break if it is wrong or missing. Read the logic before you touch
any code.

The Big Picture: Why So Many Credentials?


The tracking system connects five external platforms — GTM, Meta, Google Ads, GA4, and your own
server — into a single pipeline. Each platform requires its own authentication to accept data from you.
Think of it like this:

Platform What It Does in This System Credential You Need


Google Tag Manager Fires tracking tags on the website — page Container ID (GTM-XXXXXXX) from
(Web) views, button clicks, form submits marketer
Google Tag Manager Receives events from your CRM and relays Container Config string (eyJ...) from
(Server) them to Meta and GA4 — runs on your server marketer
Meta / Facebook Receives conversion events (KYC, deposit, Pixel ID + CAPI Access Token from
trade) and uses them to optimize ad delivery marketer
Google Analytics 4 Records every event in a reporting database Measurement ID (G-XXXXXXXXXX)
— shows funnel, attribution, campaign from marketer
performance
Google Ads Receives offline conversions (KYC, deposit) Customer ID + Conversion IDs +
via API to close the loop between ad clicks and OAuth credentials
real outcomes
Your Own Server Hosts the GTM Server Container — the central SSH access + domain DNS access
relay between CRM events and ad platforms

1.1 — Credentials You Receive FROM the Marketer


The marketer completes their setup first. Before you write a single line of code, you need these from
them. Do not proceed without all of them.

Credential Format Why You Need It Where You Use It


GTM Web Container ID GTM-XXXXXXX (8 This ID tells the browser which Website HTML <head>
chars) GTM container to load. Without it, tag
no tags fire — the whole tracking
layer is dead.
GTM Server Container Long eyJhbGci... This is the authentication key that Docker environment
Config string proves your server is allowed to variable
run this specific GTM container. CONTAINER_CONFIG
Docker will reject startup without
Credential Format Why You Need It Where You Use It
it.
Meta Pixel ID 15–16 digit number Every CAPI event you send must GTM Server CAPI tag +
include this ID so Meta knows all event payloads
which ad account to credit the
conversion to.
Meta CAPI Access Long random string This is the API key that GTM Server CAPI tag
Token authenticates your server to — stored as secret
Meta's Conversions API. Without
it, Meta rejects every event with a
401 error.
GA4 Measurement ID G-XXXXXXXXXX Tells GA4 which property to send GTM Web container
events to. Wrong ID = events go GA4 tags
nowhere or to the wrong account.
Google Ads Customer XXX-XXX-XXXX Identifies which Google Ads Google Ads API client
ID format account receives the offline initialization
conversion uploads.
Google Ads Conversion One per conversion Each conversion action (KYC uploadClickConversions
Action IDs type Approved, First Deposit, etc.) has () function
a unique ID. Wrong ID =
conversion credited to wrong
campaign.
Confirmed tracking [Link] This is the URL your server will DNS A record + Nginx
subdomain m serve GTM on. You need this config
confirmed before DNS setup.

Store all tokens and secrets in environment variables or a secrets manager — never hardcode them in
source files or commit to Git. A leaked CAPI Access Token gives anyone the ability to send fake
conversions to your Meta account.

1.2 — Google Ads OAuth Credentials (Developer Creates These)


The Google Ads offline conversion upload uses OAuth 2.0 — meaning you need to authenticate as a
developer application, not just paste a token. Here is exactly what to create and why.

WHY THIS MATTERS: Google Ads does not use simple API tokens like Meta does. It uses OAuth
because conversion uploads affect billing and campaign spend — Google wants a registered application,
not an anonymous script, making those calls.

1 Create a Google Cloud Project (just for OAuth — no hosting here)

Go to [Link] → New Project → Name it scstrade-ads-api → Create.

You are not hosting anything here. This project exists only to generate the OAuth client
credentials. Think of it as registering your application with Google.
2 Enable the Google Ads API

Inside your new project: APIs & Services → Library → search 'Google Ads API' → Enable it.

Without this, the API endpoint does not exist for your project — calls will return 404.

3 Create OAuth 2.0 Credentials

APIs & Services → Credentials → Create Credentials → OAuth client ID → Application type:
Web application → Name: SCSTrade CRM.

You will get two values: Client ID and Client Secret. Download the JSON file. Store both
securely — you cannot retrieve the secret again.

4 Get a Developer Token

Go to [Link] → Tools & Settings → API Center → Apply for developer token. Use
'Test Account' level initially — this is enough for sending real conversions. The token looks like:
XXXXXXXXXXXXXXXXXX.

The developer token is account-level, not project-level. It belongs to the Google Ads account itself. The
marketer may need to approve this from their Ads account.

5 Generate a Refresh Token

Use Google's OAuth Playground ([Link]/oauthplayground) or run the auth flow


locally once to get a long-lived refresh token. The access token expires in 1 hour — the refresh
token is what your server uses permanently.

// Scope needed:
[Link]

// After auth flow, store these as env vars:


GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REFRESH_TOKEN=your_refresh_token
GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token
GOOGLE_ADS_CUSTOMER_ID=xxx-xxx-xxxx
RESULT: After Part 1, you have every credential needed. The rest of this guide is purely technical
implementation — no more account setup.
PART 2 — SERVER SETUP
Deploy GTM Server Container on your own VPS — no Google Cloud

What Is the GTM Server Container and Why Does It Live on Your
Server?
A standard GTM Web Container runs inside the visitor's browser. That means it is vulnerable to ad
blockers, browser privacy settings, and iOS tracking restrictions. Roughly 30–40% of events can be lost
this way.

The GTM Server Container runs on YOUR server. The browser sends one request to your domain
([Link]), your server processes it, and then relays verified events directly to Meta and
GA4 over server-to-server connections. Ad blockers cannot touch server-to-server traffic.

WHY THIS MATTERS: Server-side tracking also lets you send enriched offline events — KYC approvals,
deposits, trades — that happen entirely outside the browser. These are the highest-value signals for
campaign optimization. There is no other way to send them without a server container.

Without Server Container With Server Container


Events blocked by ad blockers Events sent server-to-server — never blocked
No CRM events to ad platforms KYC, deposit, trade events reach Meta and GA4
Browser closes = event lost CRM triggers event independently of browser
Approximate 60–70% data accuracy Approximate 95%+ data accuracy

2.1 — Server Requirements


Any Linux VPS or dedicated server works. Minimum specs for light-to-medium traffic:

Requirement Minimum Recommended


OS Ubuntu 20.04+ Ubuntu 22.04 LTS
RAM 1 GB 2 GB
CPU 1 vCPU 2 vCPU
Disk 20 GB 40 GB
Open Ports 80 (HTTP), 443 (HTTPS) 80, 443, 22 (SSH)
Domain Access DNS management for [Link] Same
2.2 — Step-by-Step: Deploy GTM Server Container

1 SSH into your server

ssh root@[Link]
# or with key:
ssh -i ~/.ssh/your_key.pem ubuntu@[Link]

Everything from here runs on the server, not your local machine.

2 Install Docker and Docker Compose

Docker is the container runtime. The GTM Server image runs inside a Docker container —
isolated, easy to restart, and does not conflict with other software on your server.

sudo apt-get update


sudo apt-get install -y [Link] docker-compose
sudo systemctl start docker
sudo systemctl enable docker

# Verify:
docker --version
# Expected: Docker version 24.x.x or higher

3 Create the [Link] file

Replace eyJhbGci... with the exact Container Config string the marketer gave you. Do not
wrap it in quotes inside the YAML — just paste the raw string.

mkdir -p /opt/gtm-server
cd /opt/gtm-server
nano [Link]

# Paste this content:


version: '3'
services:
gtm-server:
image: [Link]/cloud-tagging-10302018/gtm-cloud-image:stable
environment:
- CONTAINER_CONFIG=eyJhbGci...YOUR_FULL_CONFIG_STRING_HERE
- PORT=8080
- PREVIEW_SERVER_URL=[Link]
ports:
- '8080:8080'
restart: always
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'

The restart: always line means if your server reboots, GTM comes back up automatically without
manual intervention.

4 Start the container

docker-compose up -d

# Check it started:
docker-compose ps
# Expected: gtm-server Up [Link]:8080->8080/tcp

# Check logs for errors:


docker-compose logs --tail=50
# Expected: 'Server started on port 8080'

The -d flag runs it in detached mode (background). If you see any CONTAINER_CONFIG errors
in the logs, the config string was pasted incorrectly — most likely a whitespace issue.

5 Install Nginx as reverse proxy

Docker exposes GTM on port 8080 internally. Nginx sits in front and handles the public HTTPS
traffic on port 443, then passes requests to port 8080. This is the standard production pattern.

sudo apt-get install -y nginx

# Create config file:


sudo nano /etc/nginx/sites-available/analytics

# Paste this:
server {
listen 80;
server_name [Link];

location / {
proxy_pass [Link]
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Enable it:
sudo ln -s /etc/nginx/sites-available/analytics /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

6 Set up DNS — point subdomain to your server

Go to wherever [Link] DNS is managed (your domain registrar or hosting panel —


Namecheap, GoDaddy, Cloudflare, cPanel, etc.) and add:

Record Type Name / Host Value / Points To TTL


A Record analytics [Link] 300 (5 min)

TTL 300 means DNS changes propagate within 5 minutes. After adding, wait 5 minutes before
proceeding.

7 Install SSL certificate (HTTPS)

HTTPS is not optional. Meta and Google reject events from non-HTTPS endpoints. Let's
Encrypt provides a free, auto-renewing certificate.

sudo apt-get install -y certbot python3-certbot-nginx

# Get certificate — replaces HTTP config with HTTPS automatically:


sudo certbot --nginx -d [Link]

# Follow prompts: enter email, agree to terms, choose redirect option.

# Verify auto-renewal works:


sudo certbot renew --dry-run
# Expected: Congratulations, all simulated renewals succeeded

8 Verify the server is live

# From your local machine:


curl -I [Link]
# Expected: HTTP/2 200 or HTTP/2 302

# Also open in browser:


# [Link]
# You should see a GTM response — not an Nginx error page.
RESULT: The server is live. [Link] is now a working GTM Server endpoint. Inform the
marketer — they need this URL to configure their GTM Server container tags.
PART 3 — WEBSITE CODE
GTM snippet, visitor ID, UTM capture, button events, form tracking

How the Website Layer Works


The website code does three things: (1) loads GTM so the marketer's tags fire, (2) assigns every visitor a
unique ID (SCS_UID) that persists across sessions, and (3) captures the ad parameters from the URL so
attribution is preserved even if the visitor leaves and comes back later.

WHY THIS MATTERS: Without the SCS_UID, you cannot connect a website visitor to a CRM lead to a
trading account. It is the thread that runs through the entire funnel. Without UTM capture on the first visit,
you lose attribution for any lead that does not convert on the same session they clicked the ad.

3.1 — Install GTM Snippets


The marketer gives you a GTM Container ID (GTM-XXXXXXX). These two snippets go into every page of
the website.

<!-- SNIPPET 1: Inside <head> tag, as high as possible -->


<script>
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'[Link]': new Date().getTime(), event:'[Link]'});
var f=[Link](s)[0],
j=[Link](s),
dl=l!='dataLayer'?'&l='+l:'';
[Link]=true;
[Link]='[Link]
[Link](j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX'); // ← replace with real ID
</script>

<!-- SNIPPET 2: Immediately after opening <body> tag -->


<noscript>
<iframe src='[Link]
height='0' width='0' style='display:none;visibility:hidden'>
</iframe>
</noscript>

Snippet 1 loads the GTM JavaScript library asynchronously — it does not slow down page load. Snippet 2
is a fallback for browsers with JavaScript disabled (rare, but Meta and Google require it for compliance).

3.2 — Cookie Helper Functions


These are utility functions used by everything else. Add them to a shared JS file loaded on every page.
function setCookie(name, value, days) {
const expires = new Date([Link]() + days * 864e5).toUTCString();
[Link] = name + '=' + encodeURIComponent(value)
+ ';expires=' + expires
+ ';path=/'
+ ';SameSite=Lax';
}

function getCookie(name) {
const match = [Link]
.split('; ')
.find(r => [Link](name + '='));
return match ? decodeURIComponent([Link]('=')[1]) : '';
}

3.3 — Unique Visitor ID (SCS_UID)


Every visitor gets a unique ID the first time they land on the site. It is stored in a cookie for 365 days. If the
same visitor returns a year later, they get the same ID — their full history stays connected.

function getSCSUID() {
let uid = getCookie('SCS_UID');
if (!uid) {
// Generate: prefix + unix timestamp + random alphanumeric
uid = 'SCS_' + [Link]() + '_' + [Link]().toString(36).substr(2, 9);
setCookie('SCS_UID', uid, 365);
}
return uid;
// Example output: SCS_1717200000000_k3m9xpq2a
}

When a lead submits a form, this SCS_UID goes into the CRM. When the same person eventually makes
a deposit, the trading platform looks up their SCS_UID and sends the correct attribution data to Meta and
Google.

3.4 — UTM and Ad Parameter Capture


When a visitor arrives from an ad, the URL contains parameters like ?
utm_source=facebook&utm_campaign=june_promo&fbclid=IwAR... This function reads them all and
stores them in cookies for 30 days.

WHY THIS MATTERS: The fbclid (Facebook Click ID) and gclid (Google Click ID) are critical for match
quality. Meta uses fbclid to match your server event back to the exact person who clicked the ad — even if
they use a different browser or device later. Without capturing and sending these back, match quality drops
significantly.

function captureAttributionData() {
const p = new URLSearchParams([Link]);

const data = {
utm_source: [Link]('utm_source') || getCookie('utm_source'),
utm_medium: [Link]('utm_medium') || getCookie('utm_medium'),
utm_campaign: [Link]('utm_campaign') || getCookie('utm_campaign'),
utm_content: [Link]('utm_content') || getCookie('utm_content'),
utm_term: [Link]('utm_term') || getCookie('utm_term'),
fbclid: [Link]('fbclid') || getCookie('fbclid'),
gclid: [Link]('gclid') || getCookie('gclid'),
landing_page: [Link],
first_visit: getCookie('first_visit') || new Date().toISOString(),
scs_uid: getSCSUID()
};

// Save all values to cookies (30 days)


// This means: if visitor leaves and comes back later without UTM params,
// the original attribution is still preserved in cookies.
[Link](data).forEach(k => {
if (data[k]) setCookie(k, data[k], 30);
});

return data;
}

// Run immediately on every page load:


const attrData = captureAttributionData();

3.5 — CTA Button Click Tracking


Add a data attribute to every conversion button — Open Account, Register Now, Start Trading, etc. The
JavaScript listener watches for clicks and pushes an event to the GTM dataLayer.

<!-- HTML: Add data-track and data-cta-name to every conversion button -->
<button data-track='cta_click' data-cta-name='Open Account'>
Open Account
</button>

<button data-track='cta_click' data-cta-name='Register Now'>


Register Now
</button>

// JavaScript: Single listener handles all CTA buttons


[Link]('[data-track="cta_click"]').forEach(btn => {
[Link]('click', function() {
[Link] = [Link] || [];
[Link]({
event: 'cta_click',
cta_name: [Link],
scs_uid: attrData.scs_uid,
utm_source: attrData.utm_source,
utm_campaign: attrData.utm_campaign,
fbclid: [Link],
gclid: [Link],
});
// GTM picks this up and fires GA4 + Meta tags automatically
});
});
3.6 — Lead Form Submission
When the lead form is submitted, two things happen simultaneously: the event goes to GTM's dataLayer
(for GA4 and Meta Pixel), and the form data is sent to your CRM API (covered in Part 4).

[Link]('lead-form').addEventListener('submit', function(e) {
// Do NOT call [Link]() unless you handle form submission manually

const attrData = captureAttributionData();

const formData = {
event: 'lead_submitted',
lead_name: [Link]('name').value,
lead_phone: [Link]('phone').value,
lead_email: [Link]('email').value,
scs_uid: attrData.scs_uid,
utm_source: attrData.utm_source,
utm_medium: attrData.utm_medium,
utm_campaign: attrData.utm_campaign,
utm_content: attrData.utm_content,
fbclid: [Link],
gclid: [Link],
landing_page: attrData.landing_page
};

// 1. Push to dataLayer — GTM fires GA4 and Meta Pixel tags


[Link] = [Link] || [];
[Link](formData);

// 2. Send to CRM API (see Part 4)


sendToCRM(formData);
});

Phone numbers must be stored in international format: +92XXXXXXXXXX. Meta's matching algorithm
requires this format to identify users cross-device. A number stored as 0300XXXXXXX will not match.
PART 4 — CRM INTEGRATION
Database schema, lead creation API, pipeline stages

Why the CRM Is the Core of This System


The CRM is not just a place to store contacts. In this architecture, it is the single source of truth for
attribution data. Every conversion event that goes to Meta and Google originates from the CRM — the
website only captures the initial data, the CRM tracks everything that happens afterward.

WHY THIS MATTERS: When a sales agent moves a lead to 'KYC Approved', that is the moment the CRM
fires a conversion event to Meta with the original ad click data from 3 weeks ago. The CRM holds the fbclid,
gclid, and utm_campaign from the first visit. Without storing these in the CRM, offline conversions are
impossible.

4.1 — Database Schema Changes


Add these columns to your existing leads table. If you are starting fresh, include them in the initial
CREATE TABLE statement.

ALTER TABLE leads


ADD COLUMN scs_uid VARCHAR(100) COMMENT 'Unique visitor ID from website
cookie',
ADD COLUMN utm_source VARCHAR(100) COMMENT 'Traffic source: facebook, google,
tiktok',
ADD COLUMN utm_medium VARCHAR(100) COMMENT 'Channel: paid_social, cpc,
email',
ADD COLUMN utm_campaign VARCHAR(200) COMMENT 'Campaign name from marketer UTM',
ADD COLUMN utm_content VARCHAR(200) COMMENT 'Ad creative identifier',
ADD COLUMN utm_term VARCHAR(200) COMMENT 'Keyword — Google Search only',
ADD COLUMN fbclid VARCHAR(500) COMMENT 'Facebook Click ID — send unhashed
to Meta',
ADD COLUMN gclid VARCHAR(500) COMMENT 'Google Click ID — used for
offline conv upload',
ADD COLUMN fbp_cookie VARCHAR(200) COMMENT 'Facebook browser cookie _fbp',
ADD COLUMN landing_page TEXT COMMENT 'First URL the visitor landed on',
ADD COLUMN first_visit_at DATETIME COMMENT 'Timestamp of first website
visit',
ADD COLUMN current_stage VARCHAR(50) DEFAULT 'New Lead',
ADD COLUMN trading_account_id VARCHAR(100) COMMENT 'Links to trading platform
account';

-- Add index on scs_uid for fast lookups:


CREATE INDEX idx_scs_uid ON leads (scs_uid);
CREATE INDEX idx_trading_account ON leads (trading_account_id);

The index on scs_uid matters. When a deposit comes in from the trading platform, the system needs to
look up the original lead by trading_account_id instantly. Without indexes, this slows down as your
database grows.
4.2 — Lead Creation API Endpoint
This endpoint receives form submissions from the website and creates a lead record with all attribution
data attached.

// POST /api/leads/create
[Link]('/api/leads/create', async (req, res) => {
try {
const {
lead_name, lead_phone, lead_email,
scs_uid, utm_source, utm_medium, utm_campaign,
utm_content, utm_term, fbclid, gclid, landing_page
} = [Link];

// Normalize phone to international format


const phone = normalizePhone(lead_phone);
const email = lead_email.toLowerCase().trim();

// Check for duplicate — same phone submitted twice


const existing = await [Link](
'SELECT id FROM leads WHERE phone = ? LIMIT 1', [phone]
);
if ([Link] > 0) {
return [Link]({ success: true, duplicate: true, lead_id: existing[0].id });
}

const result = await [Link](`


INSERT INTO leads
(name, phone, email, scs_uid, utm_source, utm_medium,
utm_campaign, utm_content, utm_term, fbclid, gclid,
landing_page, current_stage, first_visit_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,'New Lead', NOW())
`, [lead_name, phone, email, scs_uid, utm_source, utm_medium,
utm_campaign, utm_content, utm_term, fbclid, gclid, landing_page]);

[Link]({ success: true, lead_id: [Link] });

} catch (err) {
[Link]('Lead create error:', err);
[Link](500).json({ success: false, error: [Link] });
}
});

// Phone normalization function:


function normalizePhone(phone) {
const digits = [Link](/\D/g, '');
if ([Link]('0')) return '+92' + [Link](1);
if ([Link]('92')) return '+' + digits;
if ([Link]('923')) return '+' + digits;
return '+92' + digits;
}

4.3 — Pipeline Stages


The sales team moves leads through these stages in the CRM. Each stage that has an Event Name will
automatically trigger a conversion event to Meta and Google when the stage changes (see Part 5).
Stag Stage Name Triggered By Fires Event
e
1 New Lead Form submission from website lead
2 Contact Attempted Sales agent marks first call attempt — (no event)
3 Interested Lead confirms interest on call interested_lead
4 Documents Requested Agent asks for ID/docs — (no event)
5 Documents Received Documents uploaded/received documents_received
6 KYC Started Compliance begins verification kyc_started
7 KYC Approved Compliance approves identity kyc_approved ← HIGH VALUE
8 Account Opened Trading account created on platform account_opened ← HIGH
VALUE
9 First Deposit Deposit confirmed by trading first_deposit ← CRITICAL
platform
10 First Trade First trade executed first_trade ← CRITICAL
11 Active Trader Regular trading activity confirmed active_trader ← CRITICAL

Stages 1–6 help the sales team track pipeline. Stages 7–11 are what the ad platforms actually optimize for.
Meta will learn: 'show this ad to people who look like the ones who reached Stage 9' — but only if the
events reach Meta reliably.
PART 5 — EVENT SYSTEM
Stage-change events, SHA-256 hashing, relay to GTM Server

How the Event Flow Works


When a sales agent changes a lead's stage in the CRM, the following happens automatically — no
manual action required:

Step What Happens Where


1 Agent clicks 'Move to KYC Approved' in CRM UI CRM frontend
2 CRM backend updates the stage in the database CRM backend
3 updateLeadStage() function fires automatically CRM backend
4 Function builds an event payload with attribution data from CRM backend
the lead record
5 Payload is hashed (email, phone) and sent to CRM backend → Your GTM Server
[Link]/mp/collect
6 GTM Server processes the event and forwards to Meta CAPI Your GTM Server → Meta / GA4
and GA4
7 Meta logs a 'CompleteRegistration' conversion tied to the Meta servers
original ad click
8 GA4 logs a 'kyc_approved' event with utm attribution GA4 servers

5.1 — SHA-256 Hashing


Meta and Google require that personally identifiable information (email, phone number) be hashed before
transmission. SHA-256 is a one-way hash — it cannot be reversed, but Meta's servers can hash their
own user data using the same algorithm and find a match.

WHY THIS MATTERS: You MUST hash email and phone. Sending raw PII violates GDPR, Meta's terms
of service, and exposes your users to data risk. However, fbclid and the fbp cookie must be sent
UNHASHED — Meta needs the raw values to do the matching. Hashing them breaks attribution.

const crypto = require('crypto');

function hashSHA256(value) {
if (!value) return '';
// Normalize before hashing: lowercase, remove whitespace
const normalized = [Link]().trim();
return [Link]('sha256').update(normalized).digest('hex');
}

// Phone must be in E.164 format before hashing:


// hashSHA256('+923001234567') — correct
// hashSHA256('0300-123-4567') — wrong: will not match Meta's hash

// Examples:
// hashSHA256('user@[Link]') → 'b94d27b9934d3e08...'
// hashSHA256('+923001234567') → 'a8f5f167f44f4964...'

5.2 — Stage-to-Event Mapping


function stageToEventName(stage) {
const map = {
'New Lead': 'lead',
'Interested': 'interested_lead',
'Documents Received':'documents_received',
'KYC Started': 'kyc_started',
'KYC Approved': 'kyc_approved',
'Account Opened': 'account_opened',
'First Deposit': 'first_deposit',
'First Trade': 'first_trade',
'Active Trader': 'active_trader',
};
return map[stage] || null;
}

5.3 — Stage Change Function (Core of the Event System)


This function is called every time a lead's stage changes. Hook it into whatever mechanism your CRM
uses for stage updates — a button click handler, an API endpoint, or a webhook.

async function updateLeadStage(leadId, newStage) {


// Step 1: Update database
const lead = await [Link](
`UPDATE leads
SET current_stage = ?, updated_at = NOW()
WHERE id = ?`,
[newStage, leadId]
);

// Step 2: Get full lead record (we need attribution fields)


const fullLead = await [Link](
'SELECT * FROM leads WHERE id = ? LIMIT 1', [leadId]
);
if (!fullLead[0]) return;
const l = fullLead[0];

// Step 3: Check if this stage fires an event


const eventName = stageToEventName(newStage);
if (!eventName) return; // stages like 'Contact Attempted' fire nothing

// Step 4: Build payload


const payload = {
event_name: eventName,
lead_id: [Link],
scs_uid: l.scs_uid,
// PII — hashed:
email_hash: hashSHA256([Link]),
phone_hash: hashSHA256([Link]),
// Click IDs — UNHASHED (Meta and Google need raw values):
fbclid: [Link],
fbp: l.fbp_cookie,
gclid: [Link],
// Attribution:
utm_source: l.utm_source,
utm_campaign: l.utm_campaign,
utm_content: l.utm_content,
// Deduplication — prevents double-counting if event fires twice:
event_id: l.scs_uid + '_' + eventName + '_' + [Link](),
timestamp: [Link]([Link]() / 1000),
};

// Step 5: Send to GTM Server


await sendToServerGTM(payload);
}

5.4 — Send to GTM Server Function


async function sendToServerGTM(payload) {
const GTM_ENDPOINT = '[Link]

try {
const response = await fetch(GTM_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link](payload),
timeout: 5000 // 5 second timeout — do not block CRM on slow network
});

if (![Link]) {
throw new Error('GTM returned status: ' + [Link]);
}

[Link]('[GTM Event] Sent:', payload.event_name, '| Lead:', payload.lead_id);

} catch (error) {
// Log but do not crash — a failed event should not break the CRM
[Link]('[GTM Event] Failed:', payload.event_name, [Link]);

// Save to a retry queue so the event is not permanently lost


await [Link](
'INSERT INTO event_retry_queue (payload, attempts, created_at) VALUES
(?,0,NOW())',
[[Link](payload)]
);
}
}

Create an event_retry_queue table in your database. Set up a cron job that runs every 5 minutes to retry
any failed events. Events that fail permanently (e.g., invalid payload) should be logged for investigation, not
retried forever — set a max of 3 attempts.
PART 6 — TRADING PLATFORM EVENTS
First deposit and first trade — the highest-value signals

Why These Events Are Different


Stages 1 through 8 are moved manually by the sales team. Stages 9 (First Deposit) and 10 (First Trade)
must be triggered automatically by the trading platform backend — because the trading platform is the
system of record for money and trades, not the CRM.

WHY THIS MATTERS: If a deposit event fires 3 hours late because someone forgot to update the CRM
manually, Meta's attribution window may have already closed. Automating from the trading platform
ensures the event fires within seconds of the actual deposit — maximizing attribution accuracy and ROAS
reporting.

6.1 — Trading Account Linkage


When a trading account is created for a lead (Stage 8 — Account Opened), store the trading platform's
account ID in the CRM lead record. This is the bridge between the two systems.

// Called when compliance creates the trading account:


async function linkTradingAccount(leadId, tradingAccountId) {
await [Link](
'UPDATE leads SET trading_account_id = ? WHERE id = ?',
[tradingAccountId, leadId]
);
// Also update stage to Account Opened:
await updateLeadStage(leadId, 'Account Opened');
}

6.2 — First Deposit Event


Add this call to the trading platform's deposit confirmation callback — the function that already runs when
a deposit clears successfully.

async function onDepositConfirmed(tradingAccountId, amount, currency = 'PKR') {


// Look up the CRM lead by trading account ID
const leads = await [Link](
'SELECT * FROM leads WHERE trading_account_id = ? LIMIT 1',
[tradingAccountId]
);

if (!leads[0]) {
[Link]('No CRM lead found for trading account:', tradingAccountId);
return; // Log this — it means the linkage was not set up correctly
}
const lead = leads[0];

// Check if this is truly the FIRST deposit


if (lead.current_stage === 'First Deposit' ||
lead.current_stage === 'First Trade' ||
lead.current_stage === 'Active Trader') {
return; // Already past this stage — do not fire duplicate
}

// Update stage — this triggers the event automatically via updateLeadStage()


await updateLeadStage([Link], 'First Deposit');

// Also send deposit amount separately (updateLeadStage does not include it):
await sendToServerGTM({
event_name: 'first_deposit',
scs_uid: lead.scs_uid,
email_hash: hashSHA256([Link]),
phone_hash: hashSHA256([Link]),
fbclid: [Link],
gclid: [Link],
utm_campaign: lead.utm_campaign,
deposit_amount: amount,
currency: currency,
event_id: lead.scs_uid + '_deposit_' + [Link](),
timestamp: [Link]([Link]() / 1000),
});
}

6.3 — First Trade Event


async function onTradeExecuted(tradingAccountId) {
const leads = await [Link](
'SELECT * FROM leads WHERE trading_account_id = ? LIMIT 1',
[tradingAccountId]
);

if (!leads[0]) return;
const lead = leads[0];

// Only fire if not already at First Trade or Active Trader


if (['First Trade', 'Active Trader'].includes(lead.current_stage)) return;

await updateLeadStage([Link], 'First Trade');


}
PART 7 — GOOGLE ADS OFFLINE
CONVERSIONS
Closing the attribution loop for Google Search and Display campaigns

Why This Is Separate from Meta


Meta uses the CAPI (which GTM Server handles). Google Ads uses a completely different mechanism —
the Offline Conversions API — which requires uploading the gclid (Google Click ID) along with the
conversion action and value.

WHY THIS MATTERS: Without offline conversion upload, Google Ads only knows about form submissions
— not KYC approvals, deposits, or trades. This means Google's Smart Bidding algorithm optimizes for
leads, not for the people who actually deposit money. Uploading offline conversions lets Google learn what
a high-value user looks like at the click level.

7.1 — Install the Google Ads API Client


npm install google-ads-api

7.2 — Environment Variables


# In your .env file or server environment:
GOOGLE_ADS_DEVELOPER_TOKEN=XXXXXXXXXXXXXXXXXX
GOOGLE_ADS_CUSTOMER_ID=XXX-XXX-XXXX
GOOGLE_CLIENT_ID=your_oauth_client_id.[Link]
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx
GOOGLE_REFRESH_TOKEN=1//0gxxxxxxxxxxxxxxxxxx

7.3 — Upload Offline Conversion Function


const { GoogleAdsApi } = require('google-ads-api');

const googleAdsClient = new GoogleAdsApi({


client_id: [Link].GOOGLE_CLIENT_ID,
client_secret: [Link].GOOGLE_CLIENT_SECRET,
developer_token: [Link].GOOGLE_ADS_DEVELOPER_TOKEN,
});

async function uploadGoogleConversion(lead, conversionActionName, value = 0) {


// Only runs if this lead came from a Google Ads click
if (![Link]) {
[Link]('No gclid — lead did not come from Google Ads, skipping');
return;
}

try {
const customer = [Link]({
customer_id: [Link].GOOGLE_ADS_CUSTOMER_ID,
refresh_token: [Link].GOOGLE_REFRESH_TOKEN,
});

await [Link]({
conversions: [{
gclid: [Link],
conversion_action:
`customers/${[Link].GOOGLE_ADS_CUSTOMER_ID}/conversionActions/$
{conversionActionName}`,
conversion_date_time: new Date().toISOString().replace('T', ' ').replace('Z',
'+00:00'),
conversion_value: value,
currency_code: 'PKR',
}],
partial_failure: true,
});

[Link]('[Google Ads] Uploaded conversion:', conversionActionName, 'for lead:',


[Link]);

} catch (err) {
[Link]('[Google Ads] Upload failed:', [Link]);
}
}

// Usage — call these from inside updateLeadStage():


// await uploadGoogleConversion(lead, 'KYC_Approved', 500);
// await uploadGoogleConversion(lead, 'First_Deposit', lead.deposit_amount);
// await uploadGoogleConversion(lead, 'Active_Trader', 5000);
PART 8 — TESTING CHECKLIST
Verify every layer before going live. One broken layer = wrong attribution data forever.

Testing Sequence — Do Not Skip Steps


Test in this exact order. Each layer depends on the one before it. Testing the CRM events before verifying
the server is running will waste time.

Layer 1: GTM Server


Test How to Test Expected Result
Server is reachable curl -I [Link] HTTP/2 200 response
SSL is valid Open URL in browser — check padlock Green padlock, no certificate
icon warnings
Docker container running SSH to server → docker-compose ps gtm-server Up
Container logs clean docker-compose logs --tail=20 No ERROR lines — 'Server started'
visible

Layer 2: Website Tracking


Test How to Test Expected Result
GTM loads on page Open site → F12 → Network tab → filter [Link] loads with status 200
'[Link]'
SCS_UID is set Open site → F12 → Application → Cookie exists: SCS_1234...
Cookies → look for SCS_UID
UTM capture works Visit site with ?utm_source=test in URL utm_source=test cookie is set
→ check cookies
fbclid captured Visit with ?fbclid=testid123 in URL → fbclid cookie stores testid123
check cookies
dataLayer fires on CTA click F12 → Console → type: dataLayer → cta_click event visible in
click a CTA button dataLayer array
Form submission event Submit lead form → check dataLayer lead_submitted event with all
fields present

Layer 3: CRM
Test How to Test Expected Result
Lead creation API POST /api/leads/create with test data 200 response, lead_id returned, row
(test@[Link], +92300 0000000) in DB
Attribution data saved Check DB row after test submission fbclid, utm_source, scs_uid columns
are populated
Test How to Test Expected Result
Phone normalization Submit with 0300-123-4567 format Stored as +923001234567 in DB
Duplicate handling Submit same phone twice Second call returns duplicate:true,
no new row
Stage change works Manually update a test lead's stage via DB row updates, console shows
CRM UI Event sent log

Layer 4: Event Pipeline


Test How to Test Expected Result
CRM event reaches GTM Change test lead stage to 'KYC docker-compose logs shows
Server Approved' → check server logs event received
GA4 receives event GA4 → Reports → Real-time → Events kyc_approved event visible in
real-time
Meta receives event Meta Events Manager → Test Events tab Event visible in test events within
→ change stage 30 seconds
Meta match quality Meta Events Manager → Data Sources Match Quality Score 6 or above
→ Pixel → Overview
Event deduplication Trigger same event twice for same lead GA4 and Meta show it only once
(event_id check)

Layer 5: Trading Platform Integration


Test How to Test Expected Result
trading_account_id linkage Create test account → check lead record trading_account_id field
in DB populated
Deposit event fires Trigger test deposit from trading platform Lead stage changes to First
sandbox Deposit, event sent log
Deposit amount in payload Check GTM Server logs for deposit event deposit_amount field present
with correct value
Google Ads upload (if gclid exists) Use a test lead with a gclid value → Google Ads API success log —
trigger deposit no partial_failure errors

Always use test data: email test@[Link], phone +92300 0000000, name Test User. Never run integration
tests against real lead data. If a test event fires to Meta, it will try to match against real user profiles.

Common Errors and Fixes


Error Likely Cause Fix
GTM Server returns 502 Docker container not running or crashed SSH → docker-compose ps →
docker-compose up -d
GTM Server returns 503 CONTAINER_CONFIG string is wrong or Check [Link] —
truncated paste the full eyJ... string
Error Likely Cause Fix
Meta events not appearing CAPI Access Token is wrong or expired Re-generate token in Meta
Business Manager → update
GTM Server tag
Meta match quality below 4 fbclid not being sent, or phone not in Verify fbclid is captured in cookies
E.164 format and stored in CRM — verify
phone normalization
GA4 events missing GA4 Measurement ID is wrong in GTM Check GTM tag — G-
XXXXXXXXXX must match GA4
property
Google Ads upload fails with Refresh token expired or OAuth Re-run OAuth flow to get a fresh
AUTH_ERROR credentials wrong refresh token
Lead created but no attribution Website captureAttributionData() not Ensure the script runs on page
data running before form submit load — check browser console for
JS errors
trading_account_id lookup Account was created before Check trading platform webhook
returns null linkTradingAccount() was called — ensure it calls
linkTradingAccount() on account
creation

You might also like