Google Enhanced Conversions & Meta CAPI Setup (2026)
Ishant
Published : July 2, 2026 at 5:46 pm
Updated : June 10, 2026 at 5:46 pm
Ishant
Ishant Sharma is the Founder and CEO of Hustle Marketers, a Google Partner digital marketing agency. With 12+ years of experience in Google Ads, Meta Ads, SEO, and e-commerce PPC, he has helped 2500+ brands generate $780M+ in trackable revenue. Upwork Top Rated Plus with 99% Job Success Score. Ishant Sharma is the digital marketing specialist, not the Indian cricketer of the same name.
Summarize this blog post with:
Google Enhanced Conversions and Meta CAPI are the two pieces that decide whether your server-side tracking actually improves ROAS or just looks good in a deck. Most setup guides cover the GUI flow and call it done, then advertisers wonder why match quality stays at 4/10 and Smart Bidding refuses to scale. This guide shows the full production setup for both: SHA-256 hashing with the exact PII normalization rules Google and Meta require, event deduplication between browser pixel and server CAPI, direct API calls (not just GTM tags), and the April 2026 Google update that unified Enhanced Conversions for Web and Leads into a single setting. The code blocks are paste-and-deploy. Pair this with our Shopify server side tracking setup and server side GTM hosting guide for the complete stack.
What are Google Enhanced Conversions and Meta CAPI?
Google Enhanced Conversions and Meta CAPI are first-party-data conversion tracking methods that send hashed customer information (email, phone, name, address) directly from your server to the ad platform, instead of relying on browser cookies. Google Enhanced Conversions sends hashed PII to Google Ads alongside conversion events, where Google matches it against logged-in Google accounts. Meta CAPI (the Conversions API) sends the same kind of hashed data to Meta, where it matches against Facebook and Instagram user accounts.
The technical short version
Both systems work on the same principle: you collect customer data at conversion time (purchase, sign-up, lead form submission), apply a normalization layer that strips whitespace, lowercases emails, and formats phone numbers consistently, then hash the normalized values with SHA-256 before sending them to Google or Meta. The ad platform hashes its own user database the same way, compares the two hash sets, and attributes conversions when matches occur. The customer’s raw data never leaves your server.
Why both at once instead of just one
Most stores end up deploying both because they’re running ads on both platforms, and the underlying infrastructure (a GTM Server container, a customer data pipeline, a hashing utility) is identical. Building one without the other duplicates the work and creates an attribution blind spot on whichever platform you skipped. The right pattern is a single server-side stack that hashes once and dispatches to both APIs in parallel.
Why do Google Enhanced Conversions and Meta CAPI matter for ROAS?
Google Enhanced Conversions and Meta CAPI matter for ROAS because Smart Bidding (on Google) and Advantage+ (on Meta) optimize against the conversion data they receive, and both algorithms become significantly more efficient when match rates climb past 70%. Across our agency’s ecommerce PPC client portfolio, the difference between a 35% match rate (typical for browser-only tracking) and a 75%+ match rate (typical after proper server-side setup) translates into 15-30% better ROAS on the same ad spend.
The Smart Bidding feedback loop
When Google Smart Bidding receives complete conversion data, it can identify the user, device, and creative patterns that actually drive purchases. With incomplete data (missed iOS conversions, ad-blocked events, cross-device journeys), the algorithm optimizes against noise. The same logic applies to Meta Advantage+ Shopping. Better signal in equals better bidding out, and Enhanced Conversions plus CAPI are the cleanest way to improve the signal.
The Customer Match audience uplift nobody mentions
Beyond direct attribution, Enhanced Conversions feeds Google’s Customer Match system, which builds lookalike and similar-customer audiences from your converters. Properly implemented Enhanced Conversions can grow your Customer Match audiences by 40-60% versus standard tag setup, which feeds Performance Max and Demand Gen with richer seed audiences. Meta CAPI delivers the same uplift to Meta Custom Audiences. Most blogs treat Enhanced Conversions and CAPI as attribution tools only; in practice, the audience uplift compounds the value further.
How do Google Enhanced Conversions compare to Meta CAPI?
Google Enhanced Conversions and Meta CAPI share the same core mechanism (hashed first-party data matching) but differ in implementation details, required fields, and how they behave when match quality is low. The table below summarizes the practical differences our team encounters when deploying both side by side.
| Dimension | Google Enhanced Conversions | Meta CAPI |
|---|---|---|
| Endpoint | googleads.googleapis.com | graph.facebook.com/{pixel_id}/events |
| Authentication | OAuth 2.0 + developer token | Access token (long-lived) |
| Hashing requirement | SHA-256 of normalized PII | SHA-256 of normalized PII |
| Required minimum data | Email OR phone OR address | Email OR phone (one of) |
| Deduplication mechanism | Order ID (transaction_id) | event_id (must match browser pixel) |
| Click ID parameter | gclid, gbraid, wbraid | fbc (built from fbclid) |
| Cookie parameter | Not required | fbp (Facebook browser ID) |
| Match quality metric | Diagnostic % | Event Match Quality (1-10) |
| Test mode | Built-in diagnostic dashboard | test_event_code parameter |
| Implementation options | gtag.js, GTM, Google Ads API | Meta Pixel + CAPI, GTM Server, direct API |
| Best fit for ecommerce | GTM Server tag + Google Ads API | GTM Server tag + browser Pixel for dedup |
The deduplication pattern works opposite ways
Meta CAPI assumes you’re sending events from both the browser pixel AND the server CAPI, and uses event_id to deduplicate the matched pair. You generate one event_id at the moment of conversion, send it from the browser pixel and from the server CAPI, and Meta picks whichever arrives with better match data. Google Enhanced Conversions, in contrast, uses the order ID (transaction_id) for deduplication and doesn’t expect parallel browser-and-server tracking. This means your event_id strategy needs to handle both patterns simultaneously, which we cover in the code section below.
What changed with Google Enhanced Conversions in April 2026?
In April 2026 Google merged Enhanced Conversions for Web and Enhanced Conversions for Leads into a single unified setting inside Google Ads. Before April 2026, advertisers had to choose between the two implementations and pick one matching method per conversion action. After April 2026, the setting is a single on-off toggle, and Google Ads simultaneously accepts user-provided data from website tags, Data Manager, and API connections. Most existing setup guides predate this change and still walk through the old dual-path flow, which is now obsolete.
What this means in practice for your setup
If you’re starting Enhanced Conversions from scratch today, you only need to enable the unified setting once per conversion action and connect whichever data sources make sense for your business. You don’t need to maintain separate Web and Leads configurations. If you’ve previously set up Enhanced Conversions for Web, your configuration was automatically migrated by Google in April 2026, but the migration sometimes silently disabled secondary data sources that no longer fit the new model. Worth auditing.
How to verify your account was migrated correctly
Log into Google Ads, navigate to Goals then Conversions then Summary, and click any conversion action. Scroll to the Enhanced Conversions section. If you see a single toggle labeled “Turn on enhanced conversions” with multiple data source options below (Google tag, Data Manager, API), you’re on the new unified system. If you still see separate Web and Leads tabs, your account hasn’t migrated yet, and Google’s diagnostic alert system will prompt you within 30 days. Either way, the setup steps below work for both pre and post-migration accounts.
Almost every Enhanced Conversions guide published before April 2026 (which is most of them) tells you to pick between Web and Leads, set them up separately, and toggle them independently. That advice is now obsolete. If you find a guide referencing two separate setup flows or that doesn’t mention the unified setting, treat the rest of the implementation steps with skepticism and verify against Google’s current docs.
How do you set up Google Enhanced Conversions in 2026?
Setting up Google Enhanced Conversions in 2026 takes four steps under the unified post-April model: (1) enable Enhanced Conversions on each conversion action in Google Ads and accept the customer data terms, (2) pick your primary data source from Google tag, GTM Server, or direct API, (3) implement the data collection layer that captures customer PII at conversion time, (4) verify via the diagnostic dashboard that match quality is climbing toward 70%+. Total setup time is 2-4 hours for a Shopify store using GTM Server, longer if you’re hand-rolling the API integration.
Enable in Google Ads
Goals → Conversions → pick your action → toggle Enhanced Conversions on, accept terms.
Pick your data source
Google tag for small stores, GTM Server for ecommerce, direct API for enterprise.
Implement data layer
Capture email, phone, name, address at conversion. Normalize and hash with SHA-256.
Verify diagnostics
Wait 48-72h, then check Diagnostics tab for match rate. Target 70%+ on purchase events.
Step 1: Enable Enhanced Conversions in Google Ads
Log into Google Ads, navigate to Goals then Conversions then Summary, and click the conversion action you want to enhance (typically Purchase for ecommerce or Lead for B2B). Scroll to the Enhanced Conversions section and toggle the setting to on. Accept the customer data processing terms, which is a Google legal requirement before any hashed data can be received. The toggle is now unified under the April 2026 model, so this single setting covers both purchase and lead flows for that conversion action.
Step 2: Choose your data source method
Google Ads now offers three data source paths under the unified Enhanced Conversions model, and the right choice depends on your existing stack:
| Data source | Best for | Implementation effort |
|---|---|---|
| Google tag (gtag.js) | Small stores, single conversion page, no GTM | Low (snippet on thank-you page) |
| GTM Server container | Ecommerce, multi-platform tracking | Medium (recommended for most stores) |
| Data Manager | CRM-based lead conversions | Low (CSV upload or native connectors) |
| Google Ads API direct | Enterprise, custom backend systems | High (full server-to-server build) |
For Shopify and ecommerce in general, GTM Server is the right answer 90% of the time. It plays nicely with Meta CAPI on the same infrastructure, gives you a single data pipeline to maintain, and uses Google’s own tag template so you’re not maintaining custom code.
Step 3: Implement the GTM Server tag for Enhanced Conversions
Inside your GTM Server container, create a new tag of type Google Ads Conversion Tracking. Configure the conversion ID and conversion label from the conversion action you enabled in Step 1. Then enable the User-provided data section and map each field to the variables your container creates from the incoming event payload:
| GTM field | Variable to map |
|---|---|
| Conversion ID | AW-XXXXXXXXX from your conversion action |
| Conversion Label | The label string from the same conversion action |
| Order ID | {{Event Data - transaction_id}} |
{{Event Data - email}} (unhashed; tag template hashes server-side) | |
| Phone | {{Event Data - phone}} |
| First name + last name | {{Event Data - first_name}} + {{Event Data - last_name}} |
| Address | Street, city, region, postal_code, country (all from event data) |
The Google Ads Conversion Tracking tag template handles SHA-256 hashing internally, so you send raw PII into the tag and it hashes before transmitting to Google. If you’re going the direct API route instead, you handle hashing yourself with the code in the section below.
Step 4: Verify match quality after 48-72 hours
Match quality data takes 48-72 hours to populate the diagnostic dashboard. To check, go to Goals then Conversions then click your conversion action, then click the Diagnostics tab. Look for the Enhanced Conversions row, which shows the percentage of conversions for which Google received valid user-provided data. Target 70%+ on purchase events for a healthy setup. Below 50% means something is missing in your data layer or your normalization rules are stripping valid values.
How do you set up Meta CAPI in 2026?
Setting up Meta CAPI in 2026 takes five steps: (1) generate a Conversions API access token in Meta Events Manager, (2) decide between the browser pixel + CAPI dedup pattern or CAPI-only, (3) configure your GTM Server container with the Meta CAPI tag and your access token, (4) implement event_id generation that’s identical on browser pixel and server CAPI, (5) send test events with test_event_code and verify in the Test Events tab before going live. Total setup is 2-3 hours for ecommerce on GTM Server.
Step 1: Generate the Conversions API access token
In Meta Events Manager, navigate to Data Sources, click your Pixel, then click the Settings tab. Scroll to the Conversions API section and click Generate access token. Treat this token like a password because it grants write access to your pixel data. Store it in your GTM Server container as an environment variable or in a Variable of type Constant String with the Format value option set to Sensitive so it’s never displayed in tag preview mode.
Developers often store the Meta CAPI access token in a regular GTM variable, which exposes it in Preview Mode logs and to anyone with View access to the container. Use GTM Server’s environment variable feature (set via gcloud or Stape Power-Up settings) and reference it as
process.env.META_CAPI_TOKEN inside custom templates, or use a Variable with Sensitive format. Both approaches mask the value from preview logs and from team members without admin access.Step 2: Choose the dedup pattern
You have two valid patterns for Meta CAPI. The first is browser pixel plus CAPI with event_id deduplication, where you send the same event from both sources and Meta picks the better-matched copy. Alternatively, you can run CAPI-only, removing the browser pixel entirely and relying on CAPI for all events. The browser-plus-CAPI approach delivers the best Event Match Quality scores (typically 8-9 out of 10) because Meta gets two chances to match. By contrast, the CAPI-only approach is simpler to maintain but caps your match quality around 6-7 because some browser-only signals (like fbp cookies) aren’t available to the server.
For ecommerce running paid Meta ads, browser pixel plus CAPI is almost always the right choice. The dedup mechanism is reliable, and the match quality uplift directly improves Advantage+ Shopping performance. Use the CAPI-only path only if browser-side Pixel is breaking from privacy regulations or consent constraints.
Step 3: Configure the Meta CAPI tag in GTM Server
Inside your GTM Server container, create a tag using the Meta Conversions API template (search the template gallery for “Conversions API” and pick the one published by Meta). Configure with your Pixel ID, access token (referenced via the variable from Step 1), and map the user_data and custom_data fields to your incoming event variables:
| Section | Field | Variable |
|---|---|---|
| Auth | Pixel ID | Your 15-16 digit Pixel ID |
| Auth | Access Token | {{Const - META_CAPI_TOKEN}} (Sensitive) |
| Event | Event Name | {{Event Data - event_name}} mapped via lookup table |
| Event | Event ID | {{Event Data - event_id}} (must match browser Pixel) |
| Event | Event Time | {{Event Data - event_time}} (Unix seconds) |
| User | {{Event Data - email}} (template hashes internally) | |
| User | Phone | {{Event Data - phone}} |
| User | fbp (browser ID) | {{Event Data - fbp}} |
| User | fbc (click ID) | {{Event Data - fbc}} |
| User | Client IP + UA | Auto-captured by GTM Server |
| Custom | Currency | {{Event Data - currency}} |
| Custom | Value | {{Event Data - value}} |
| Custom | contents (line items) | Mapped array of item_id, quantity, price |
Step 4: Implement matching event_id on browser pixel
For deduplication to work, the browser Meta Pixel and the server CAPI tag must send the same event_id for the same logical event. This means generating event_id once per conversion event and passing it to both. In a Shopify Custom Pixel setup (covered in our Shopify server side tracking guide), the event_id is generated when the conversion fires and gets attached both to the browser fbq call and to the payload sent to your GTM Server endpoint.
If your store still uses the legacy Meta Pixel via theme code, modify the fbq purchase call to include eventID:
// CUSTOMIZE BEFORE DEPLOYING:
// 1. Replace 'ORDER_12345' with your actual order ID variable
// (typically from Shopify's checkout.order.id or your CMS variable)
// 2. The same event_id MUST be sent server-side in the CAPI tag
//
// Browser-side Meta Pixel call with matching event_id for CAPI dedup
fbq('track', 'Purchase', {
value: 75.50,
currency: 'USD'
}, {
eventID: 'ORDER_12345' // Must match the event_id sent via CAPI
});Step 5: Send test events and verify in the Test Events tab
Before going live, generate a test event code in Events Manager (Data Sources, your Pixel, Test Events tab) and send a few test conversions. Add the test event code to your GTM Server tag’s Test Event Code field temporarily. Watch the Test Events tab in real time. Each test conversion should appear within 5-10 seconds with all user_data fields populated and the dedup match indicator showing. If you see “Not matched” warnings, the event_id isn’t aligning between browser and server, which we’ll fix in the next section’s code.
How do you hash PII for Google Enhanced Conversions and Meta CAPI?
Hashing PII for Google Enhanced Conversions and Meta CAPI requires three steps applied in this exact order: (1) normalize the raw value by trimming whitespace, lowercasing emails, and formatting phone numbers to E.164, (2) hash the normalized value with SHA-256, (3) encode the output as a lowercase hexadecimal string. Skipping any step or doing them out of order produces hashes that don’t match Google’s or Meta’s database, which silently tanks match quality to 1-2 out of 10 without any error message.
The exact normalization rules both platforms require
Google and Meta both publish normalization rules, but they’re scattered across multiple documentation pages and differ slightly per field. Here are the consolidated rules our team uses in production for both APIs:
| Field | Normalization rule | Example input → normalized |
|---|---|---|
| Trim, lowercase | " John@Email.COM " → john@email.com | |
| Phone | Strip all non-digits, prepend country code if missing | "(555) 123-4567" → 15551234567 (US) |
| First name | Trim, lowercase, remove accents/diacritics | " José " → jose |
| Last name | Trim, lowercase, remove accents/diacritics | "O'Brien" → obrien |
| City | Trim, lowercase, strip punctuation and spaces | "New York" → newyork |
| State / region | 2-letter code, lowercase | "California" → ca |
| Postal code | Trim, lowercase, no spaces; first 5 digits for US | "94103-1234" → 94103 |
| Country | 2-letter ISO 3166-1 code, lowercase | "United States" → us |
Production-ready hashing utility (Node.js)
This is the exact hashing utility we use across our agency’s GTM Server custom templates. It handles all normalization rules above, accepts both string and undefined inputs gracefully, and returns lowercase hex SHA-256 hashes ready to send to either API. Drop it into a custom template or your sGTM custom server code.
// ============================================================
// PII HASHING UTILITY for Google Enhanced Conversions + Meta CAPI
// ============================================================
// WHERE TO USE: GTM Server custom template, Cloud Run service,
// any Node.js server that prepares data for EC or CAPI APIs.
//
// CUSTOMIZE BEFORE DEPLOYING:
// 1. Default country in normalizePhone() (line 28) if your store
// isn't US-based. Change '1' to your country's calling code.
// Examples: UK '44', India '91', Australia '61', Germany '49'
//
// 2. The state codes map (line 56) covers US only. Add your
// country's state/province codes if you serve non-US customers.
//
// IMPORTANT:
// - Both APIs require lowercase hex SHA-256. Don't switch to base64.
// - Hash empty strings to empty strings (don't hash empty values).
// - Always normalize BEFORE hashing.
// ============================================================
const crypto = require('crypto');
// SHA-256 hash with lowercase hex encoding
function sha256(value) {
if (!value || typeof value !== 'string' || value.length === 0) {
return '';
}
return crypto.createHash('sha256').update(value).digest('hex');
}
// Email: trim, lowercase (Meta-compatible base normalization)
function normalizeEmail(email) {
if (!email) return '';
return email.trim().toLowerCase();
}
// Email for Google EC: trim, lowercase, AND strip dots from Gmail/googlemail local part
// Use this variant ONLY when sending to Google Enhanced Conversions, since Gmail
// treats "john.smith@gmail.com" and "johnsmith@gmail.com" as the same account.
// Meta does NOT have this rule, so don't use this variant for CAPI calls.
function normalizeEmailForGoogle(email) {
if (!email) return '';
const trimmed = email.trim().toLowerCase();
const atIndex = trimmed.lastIndexOf('@');
if (atIndex === -1) return trimmed;
const localPart = trimmed.substring(0, atIndex);
const domain = trimmed.substring(atIndex);
if (domain === '@gmail.com' || domain === '@googlemail.com') {
return localPart.replace(/\./g, '') + domain;
}
return trimmed;
}
// Phone: strip non-digits, prepend country code if missing
function normalizePhone(phone, defaultCountryCode = '1') {
if (!phone) return '';
// Strip everything except digits
let digits = phone.replace(/\D/g, '');
// Remove leading zeros
digits = digits.replace(/^0+/, '');
// Prepend country code if number doesn't start with it
// US numbers are 10 digits without country code, 11 with
if (defaultCountryCode === '1' && digits.length === 10) {
digits = defaultCountryCode + digits;
} else if (!digits.startsWith(defaultCountryCode)) {
digits = defaultCountryCode + digits;
}
return digits;
}
// Name: trim, lowercase, strip accents
function normalizeName(name) {
if (!name) return '';
return name
.normalize('NFD') // Decompose accented chars
.replace(/[\u0300-\u036f]/g, '') // Strip diacritics
.trim()
.toLowerCase()
.replace(/[^a-z0-9]/g, ''); // Remove punctuation, spaces, apostrophes
}
// City: trim, lowercase, strip spaces and punctuation
function normalizeCity(city) {
if (!city) return '';
return city.trim().toLowerCase().replace(/[^a-z0-9]/g, '');
}
// State: convert full name to 2-letter code if needed, then lowercase
const US_STATE_CODES = {
'alabama':'al','alaska':'ak','arizona':'az','arkansas':'ar','california':'ca',
'colorado':'co','connecticut':'ct','delaware':'de','florida':'fl','georgia':'ga',
'hawaii':'hi','idaho':'id','illinois':'il','indiana':'in','iowa':'ia',
'kansas':'ks','kentucky':'ky','louisiana':'la','maine':'me','maryland':'md',
'massachusetts':'ma','michigan':'mi','minnesota':'mn','mississippi':'ms','missouri':'mo',
'montana':'mt','nebraska':'ne','nevada':'nv','new hampshire':'nh','new jersey':'nj',
'new mexico':'nm','new york':'ny','north carolina':'nc','north dakota':'nd','ohio':'oh',
'oklahoma':'ok','oregon':'or','pennsylvania':'pa','rhode island':'ri','south carolina':'sc',
'south dakota':'sd','tennessee':'tn','texas':'tx','utah':'ut','vermont':'vt',
'virginia':'va','washington':'wa','west virginia':'wv','wisconsin':'wi','wyoming':'wy'
};
function normalizeState(state) {
if (!state) return '';
const trimmed = state.trim().toLowerCase();
// If it's already a 2-letter code, return it
if (trimmed.length === 2) return trimmed;
// Otherwise look up the code, fall back to trimmed lowercase value
return US_STATE_CODES[trimmed] || trimmed.replace(/[^a-z0-9]/g, '');
}
// Postal code: trim, lowercase, no spaces; US uses first 5 digits only.
// Normalizes country input to 2-letter ISO inline so 'USA', 'United States',
// or ' US ' all correctly trigger the US ZIP+4 truncation rule.
function normalizePostalCode(zip, country = 'us') {
if (!zip) return '';
const trimmed = zip.trim().toLowerCase().replace(/\s+/g, '');
const c = (country || 'us').trim().toLowerCase().substring(0, 2);
if (c === 'us') {
return trimmed.substring(0, 5);
}
return trimmed;
}
// Country: 2-letter ISO 3166-1 code, lowercase
function normalizeCountry(country) {
if (!country) return '';
return country.trim().toLowerCase().substring(0, 2);
}
// Primary hashing function that returns a complete user_data object
// ready for either Google EC or Meta CAPI
function hashUserData(rawUserData, defaultCountryCode = '1') {
return {
email: sha256(normalizeEmail(rawUserData.email)),
phone: sha256(normalizePhone(rawUserData.phone, defaultCountryCode)),
first_name: sha256(normalizeName(rawUserData.first_name)),
last_name: sha256(normalizeName(rawUserData.last_name)),
city: sha256(normalizeCity(rawUserData.city)),
state: sha256(normalizeState(rawUserData.state)),
zip: sha256(normalizePostalCode(rawUserData.zip, rawUserData.country)),
country: sha256(normalizeCountry(rawUserData.country))
};
}
module.exports = { hashUserData, sha256, normalizeEmail, normalizeEmailForGoogle, normalizePhone };How do you call the Meta Conversions API directly?
You call the Meta Conversions API directly by sending a POST request to https://graph.facebook.com/v18.0/{pixel_id}/events with a JSON body containing the event payload and your access token. The direct API approach is useful when you’ve outgrown GTM Server (high traffic, complex routing) or when you want CAPI events triggered from a backend system that isn’t routed through GTM at all (CRM, payment processor webhook, server-side checkout).
The exact request structure Meta expects
// ============================================================
// META CONVERSIONS API - Direct call from Node.js server
// ============================================================
// WHERE TO USE: Cloud Run service, Lambda, Vercel function,
// any backend that processes purchase events and needs to send
// to Meta CAPI without going through GTM Server.
//
// CUSTOMIZE BEFORE DEPLOYING:
// 1. PIXEL_ID (line 19): Your 15-16 digit Meta Pixel ID
// 2. ACCESS_TOKEN (line 20): Generate at Events Manager →
// Data Sources → your Pixel → Settings →
// Conversions API → Generate access token.
// Store as env var, never commit to source control.
// 3. TEST_EVENT_CODE (line 21): During testing, get from
// Events Manager → Test Events tab. Remove when going live.
// 4. Graph API version (line 53): v18.0 is stable but older.
// Update to a current version (v20.0 or newer) once you've
// verified your access token works with it.
//
// NODE.JS COMPATIBILITY:
// - Node 18+: Remove the require('node-fetch') line; fetch is native.
// - Node 14-17: Keep require('node-fetch') and run npm install node-fetch@2
// (v3+ is ESM-only and won't work with require).
//
// IMPORTANT:
// - Uses the hashUserData utility from the previous section.
// - event_id must match the browser Pixel's eventID for dedup.
// - event_time is Unix seconds (not milliseconds).
// ============================================================
const fetch = require('node-fetch');
const { hashUserData } = require('./hashing-utility');
const PIXEL_ID = process.env.META_PIXEL_ID;
const ACCESS_TOKEN = process.env.META_CAPI_TOKEN;
const TEST_EVENT_CODE = process.env.META_TEST_EVENT_CODE || null;
async function sendMetaCAPI(eventName, eventId, eventTime, rawUserData, customData, clientIp, clientUserAgent) {
// Hash all PII fields before sending
const userDataHashed = hashUserData(rawUserData);
const eventPayload = {
event_name: eventName, // 'Purchase', 'AddToCart', 'ViewContent', etc.
event_time: eventTime, // Unix seconds; use actual conversion time, not Date.now()
event_id: eventId, // Must match browser Pixel eventID
action_source: 'website', // Required: tells Meta this came from a website
user_data: {
em: [userDataHashed.email].filter(Boolean),
ph: [userDataHashed.phone].filter(Boolean),
fn: [userDataHashed.first_name].filter(Boolean),
ln: [userDataHashed.last_name].filter(Boolean),
ct: [userDataHashed.city].filter(Boolean),
st: [userDataHashed.state].filter(Boolean),
zp: [userDataHashed.zip].filter(Boolean),
country: [userDataHashed.country].filter(Boolean),
client_ip_address: clientIp, // Auto-captured from request
client_user_agent: clientUserAgent, // Auto-captured from request
fbc: rawUserData.fbc || null, // Facebook click ID cookie
fbp: rawUserData.fbp || null // Facebook browser ID cookie
},
custom_data: customData // value, currency, contents, content_ids, num_items
};
// Only include event_source_url if we actually have a URL; empty string fails Meta's URL validator
if (rawUserData.page_url) {
eventPayload.event_source_url = rawUserData.page_url;
}
const payload = { data: [eventPayload] };
// Add test event code only during testing
if (TEST_EVENT_CODE) {
payload.test_event_code = TEST_EVENT_CODE;
}
const url = `https://graph.facebook.com/v18.0/${PIXEL_ID}/events?access_token=${ACCESS_TOKEN}`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) {
console.error('Meta CAPI error:', result);
return { success: false, error: result };
}
console.log('Meta CAPI success:', result.events_received, 'events received');
return { success: true, response: result };
} catch (err) {
console.error('Meta CAPI request failed:', err);
return { success: false, error: err.message };
}
}
module.exports = { sendMetaCAPI };Example: calling sendMetaCAPI from a Shopify webhook handler
// Example invocation when a Shopify checkout/orders/paid webhook fires
const { sendMetaCAPI } = require('./meta-capi');
async function handlePurchaseWebhook(order, request) {
const eventId = 'order_' + order.id; // Same event_id sent to browser Pixel for dedup
// Use the order's actual creation time, not the time the webhook is processed
// (webhook delays + retries can be minutes off, which lowers Meta match quality)
const eventTime = Math.floor(new Date(order.created_at).getTime() / 1000);
// Fall back to billing address if no shipping address exists (digital products, etc.)
const address = order.shipping_address || order.billing_address || {};
// note_attributes might be undefined or empty array; the fbc/fbp cookies are written
// to it by the storefront-side JS that captures cookies before checkout
const noteAttrs = order.note_attributes || [];
const fbc = noteAttrs.find(a => a.name === 'fbc')?.value;
const fbp = noteAttrs.find(a => a.name === 'fbp')?.value;
// Chained proxies (Cloudflare, CDN, load balancer) send x-forwarded-for as a
// comma-separated list. Meta wants only the original client IP (the first one).
const xff = request.headers['x-forwarded-for'];
const clientIp = xff ? xff.split(',')[0].trim() : request.connection.remoteAddress;
await sendMetaCAPI(
'Purchase',
eventId,
eventTime,
{
email: order.email,
phone: order.phone || address.phone,
first_name: address.first_name,
last_name: address.last_name,
city: address.city,
state: address.province_code,
zip: address.zip,
country: address.country_code,
page_url: order.landing_site,
fbc: fbc,
fbp: fbp
},
{
currency: order.currency,
value: parseFloat(order.total_price),
contents: order.line_items.map(item => ({
id: String(item.product_id),
quantity: item.quantity,
item_price: parseFloat(item.price)
})),
content_ids: order.line_items.map(item => String(item.product_id)),
num_items: order.line_items.reduce((sum, item) => sum + item.quantity, 0)
},
clientIp,
request.headers['user-agent']
);
}How do you call the Google Ads Enhanced Conversions API directly?
You call the Google Ads Enhanced Conversions API directly by hitting the customers/{customer_id}:uploadClickConversions or customers/{customer_id}:uploadConversionAdjustments endpoint with an OAuth-authenticated request. The direct API path is heavier than Meta CAPI because Google requires OAuth 2.0 with a developer token and customer-level authentication. Use the official google-ads-api Node.js library to avoid manually building the OAuth dance.
Required credentials and setup
Before any Google Ads API call works, gather four credentials: (1) a developer token from your Google Ads MCC account at API Center, (2) a Google Cloud OAuth client_id and client_secret for the OAuth flow, (3) a refresh_token generated by running the OAuth consent flow once, (4) your Google Ads customer_id (the 10-digit number in the top-right of your Google Ads UI, no dashes).
// ============================================================
// GOOGLE ADS ENHANCED CONVERSIONS - Direct API call from Node.js
// ============================================================
// WHERE TO USE: Cloud Run service, backend pipeline, anywhere
// you want to send Enhanced Conversions without GTM Server.
//
// CUSTOMIZE BEFORE DEPLOYING:
// 1. CUSTOMER_ID (line 21): Your 10-digit Google Ads customer ID
// (no dashes; e.g. '1234567890' not '123-456-7890')
// 2. CONVERSION_ACTION_ID (line 22): Find at Goals → Conversions
// → your action → URL contains the ID
// 3. All four OAuth credentials must be in environment variables
//
// PREREQUISITE: npm install google-ads-api
// ============================================================
const { GoogleAdsApi } = require('google-ads-api');
const { hashUserData, sha256, normalizeEmailForGoogle } = require('./hashing-utility');
const client = new GoogleAdsApi({
client_id: process.env.GOOGLE_ADS_CLIENT_ID,
client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET,
developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN
});
const CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID;
const CONVERSION_ACTION_ID = process.env.GOOGLE_ADS_CONVERSION_ACTION_ID;
async function sendEnhancedConversion(orderId, conversionValue, currency, rawUserData, gclid) {
const customer = client.Customer({
customer_id: CUSTOMER_ID,
refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN
});
// Hash all PII fields. Override email with Google's Gmail-period-stripping variant
// so Gmail addresses with dots match Google's normalized database.
const hashed = hashUserData(rawUserData);
hashed.email = sha256(normalizeEmailForGoogle(rawUserData.email));
const clickConversion = {
conversion_action: `customers/${CUSTOMER_ID}/conversionActions/${CONVERSION_ACTION_ID}`,
conversion_date_time: new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, '+00:00'),
conversion_value: conversionValue,
currency_code: currency,
order_id: orderId, // Required for deduplication (replaces event_id concept)
gclid: gclid || undefined, // Click ID if available
user_identifiers: []
};
// Add hashed user identifiers (at least one required, more = better match)
if (hashed.email) {
clickConversion.user_identifiers.push({ hashed_email: hashed.email });
}
if (hashed.phone) {
clickConversion.user_identifiers.push({ hashed_phone_number: hashed.phone });
}
if (rawUserData.first_name && rawUserData.last_name && rawUserData.zip) {
clickConversion.user_identifiers.push({
address_info: {
hashed_first_name: hashed.first_name, // Hashed per Google spec
hashed_last_name: hashed.last_name, // Hashed per Google spec
city: rawUserData.city, // Unhashed per Google spec
state: rawUserData.state, // Unhashed per Google spec
postal_code: rawUserData.zip, // Unhashed per Google spec
country_code: rawUserData.country // Unhashed per Google spec
}
});
}
try {
const response = await customer.conversionUploads.uploadClickConversions({
conversions: [clickConversion],
partial_failure: true
});
if (response.partial_failure_error) {
console.error('Google EC partial failure:', response.partial_failure_error);
}
console.log('Google EC success:', response.results?.length, 'conversions uploaded');
return { success: true, response };
} catch (err) {
console.error('Google EC request failed:', err);
return { success: false, error: err.message };
}
}
module.exports = { sendEnhancedConversion };Google Ads API’s
address_info object has a specific hashed-vs-unhashed split that doesn’t match Meta CAPI’s pattern. Hashed fields: hashed_first_name, hashed_last_name, and hashed_street_address (if you collect street). Unhashed fields: city, state (called region), postal_code (called zip), and country_code. Per Google’s official policy: “Please don’t hash country, region, city and postcode data.” Meta CAPI is the opposite, hashing all address fields including city, state, zip, and country. Mixing up which fields to hash per platform is the single most common reason match quality stays at 1-2 out of 10. Our hashUserData utility hashes everything for Meta CAPI compatibility, then the Google EC code below selectively uses the unhashed rawUserData values for city, state, postal_code, and country_code.How do you deduplicate events between browser pixel and server CAPI?
You deduplicate events between browser pixel and server CAPI by generating a single event_id at conversion time and passing the same value to both the browser Pixel (as the eventID property on the fbq call) and the server CAPI (as the event_id field in the payload). Meta’s matching service then deduplicates the pair, keeping the version with the better user_data fields. If event_ids don’t match, Meta treats them as two separate conversions and double-counts.
The event_id pattern that works across all touchpoints
// ============================================================
// EVENT_ID GENERATION - Same value sent to browser Pixel + CAPI
// ============================================================
// WHERE TO USE: At the conversion point, BEFORE firing either
// the browser Pixel or the server CAPI.
//
// CUSTOMIZE BEFORE DEPLOYING:
// Nothing. The patterns below cover the common cases.
//
// IMPORTANT:
// - The same event_id MUST be sent to both browser and server.
// - event_id is per-event, not per-user, not per-session.
// - Order IDs make great event_ids because they're already unique.
// ============================================================
// Pattern A: Use order ID for purchases (most reliable)
function eventIdForPurchase(orderId) {
return 'order_' + String(orderId);
}
// Pattern B: Use session + timestamp for non-purchase events
function eventIdForOtherEvents(eventName, sessionId) {
return [eventName, sessionId, Date.now()].join('_');
}
// Pattern C: Pure UUID for cases where order ID isn't available
function generateEventId() {
// Browser environments have crypto.randomUUID() natively (modern browsers)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Node.js 14.17+ also has crypto.randomUUID
const { randomUUID } = require('crypto');
return randomUUID();
}
// Send to both browser Pixel and server CAPI with the same ID
function fireConversion(orderId, conversionData) {
const eventId = eventIdForPurchase(orderId);
// Browser Pixel (runs client-side)
if (typeof fbq !== 'undefined') {
fbq('track', 'Purchase', conversionData, { eventID: eventId });
}
// Server CAPI (queue for backend to send)
// Pass eventId via the same payload your GTM Server endpoint receives
fetch('https://sgtm.yourdomain.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event_name: 'Purchase',
event_id: eventId, // Same ID as the browser Pixel eventID
...conversionData
})
});
}What is event match quality and how do you improve it for Meta CAPI?
Event Match Quality is Meta’s 1-10 score that measures how reliably it can match the conversion events you send to actual Facebook and Instagram user accounts. The score appears in Events Manager under each event type and updates daily based on the user_data fields you provide. Scores below 6 mean Meta is throwing away most of your matched conversions. Scores of 8+ unlock full Advantage+ optimization power. Google has a similar metric called Diagnostic match rate, which works on the same principle but only reports a single percentage rather than a 1-10 scale.
The user_data fields that move match quality the most
| Field | Impact on Meta EMQ | Impact on Google match rate |
|---|---|---|
| +2 to +3 points | +30-50% | |
| Phone | +2 points | +10-20% |
| fbp + fbc cookies | +1 to +2 points (Meta only) | N/A |
| gclid / gbraid / wbraid | N/A | +15-25% (Google only) |
| First + last name | +0.5 point | +5-10% |
| Address (city, state, zip, country) | +0.5 to +1 point | +5-15% |
| Client IP + user agent | +1 point (Meta only) | N/A |
The fastest path to EMQ 8+
Send email plus phone plus fbp cookie plus fbc cookie plus client IP plus user agent. This combination puts most ecommerce stores at 8-9 Event Match Quality within two weeks of deployment. Adding first name, last name, and address pushes it to 9-10 but takes proportionally more engineering effort because you’re capturing more fields. The diminishing returns start at EMQ 8, so prioritize the high-impact fields first and add the rest if you’re trying to squeeze the last 10% of audience overlap.
Common reasons match quality stays below 6
If your Event Match Quality is stuck below 6 after 14 days of data, the culprit is almost always one of four things. First, the email is being hashed before it’s normalized (uppercase letters or whitespace in the raw value produce a different hash than the same email submitted clean). Second, the phone is missing the country code (Meta and Google both need international format like 15551234567 for US numbers). Third, the fbp and fbc cookies aren’t being captured server-side because the Shopify Custom Pixel sandbox blocks document.cookie access, and you’re using the legacy approach instead of browser.cookie.get(). Fourth, event_id mismatches between browser and server are causing dedup failures that Meta interprets as low-confidence events.
What are common Meta CAPI and Google Enhanced Conversions mistakes?
The most common mistakes we see across audits are: double-counting from broken event_id dedup, hashing already-hashed values, sending uppercase or accented characters, forgetting to send fbp and fbc cookies, mixing up which address fields to hash for which API, leaving test_event_code in production, and treating Enhanced Conversions and CAPI as set-and-forget when both need monthly diagnostic review.
Mistake 1: Double-counting from broken dedup
When event_ids don’t match between browser Pixel and server CAPI, Meta counts both events as separate conversions. Symptoms include Meta Events Manager showing 1.8-2x more purchases than your actual order count. The fix is the event_id pattern from the previous section: generate once at conversion time, send to both sources, audit by comparing total CAPI events received against total orders in your CMS for the same period.
Mistake 2: Hashing already-hashed values
Both Google’s GTM tag template and Meta’s CAPI tag template hash the values you send them, which means if you hash them before sending, you end up with double-hashed nonsense that matches nothing. Symptoms include Event Match Quality dropping to 1-2 within 24 hours of deployment. The fix is to send raw values into the tag templates (they hash internally) OR send pre-hashed values via the direct API approach. Never mix the two patterns in the same tag.
Mistake 3: Missing country code on phone numbers
If your store collects phone numbers without the country code (because Shopify’s default phone field doesn’t require it), Meta and Google can’t match the hash because their databases have international format. Symptoms include match rate gains from email but no incremental gain from adding phone. The fix is the normalizePhone function from the hashing section, which prepends the country code based on the customer’s billing country or a default fallback.
Mistake 4: Forgetting fbp and fbc cookies
Meta uses the fbp cookie (Facebook browser ID) and fbc cookie (Facebook click ID) as additional matching signals. Without them, you cap out at EMQ 6-7. Sending them lifts EMQ to 8-9. The Shopify Custom Pixel sandbox blocks document.cookie, which is why we use browser.cookie.get('_fbp') in our Shopify server side tracking guide.
Mistake 5: test_event_code left in production
The test_event_code field routes events to Meta’s Test Events tab instead of the live conversion stream, which means your Smart Bidding and Advantage+ optimizers receive zero conversion data. Symptoms include real purchases happening on your store but Meta Events Manager showing zero events on the Overview tab. The fix is to wrap the test_event_code in an environment variable that’s only set in dev/staging environments, never in production.
Mistake 6: Wrong field hashing per API (city, state, zip, country)
Google Ads API expects city, state, postal_code, and country_code sent UNHASHED inside address_info, while first_name, last_name, and street_address are sent HASHED (with their hashed_ prefix). Meta CAPI hashes ALL of those fields including city, state, zip, and country. Sending hashed city/state/zip to Google produces no matches. Sending unhashed city/state/zip to Meta also produces no matches because Meta’s parser expects hashes. The hashing utility from earlier in this guide hashes everything by default (Meta-compatible), and the Google EC code selectively uses the raw values where Google requires them.
How do you monitor Google Enhanced Conversions and Meta CAPI after launch?
Monitor Google Enhanced Conversions and Meta CAPI weekly during the first month and monthly after that. For Google, check Goals then Conversions then your action then Diagnostics tab; healthy match rate is 70%+ for purchase conversions. For Meta, check Events Manager then your Pixel then Overview tab; healthy Event Match Quality is 8+ for purchase events. Set up alerts in your dashboard tool (we use a custom monitor inside our ecommerce PPC service) so you catch silent drops within 24 hours instead of discovering them next quarter.
The audit query that catches 80% of issues fast
Once a month, run this audit: pull total purchases from your CMS for the past 30 days, compare against Total Conversions in Google Ads and Total Events Received in Meta Events Manager. The three numbers should be within 5% of each other after accounting for refunds and test orders. Gaps above 10% almost always trace back to a broken event_id pattern, a tag firing condition that excluded valid orders, or a normalization rule that’s dropping a high-volume customer segment.
Why choose Hustle Marketers for Google Enhanced Conversions and Meta CAPI setup?
Hustle Marketers has deployed Google Enhanced Conversions and Meta CAPI across ecommerce clients ranging from Shopify stores doing $50K/year to enterprise accounts running $200K+/month on combined Google Ads and Meta spend. We deliver the full stack: server-side hosting decision, GTM Server container, browser Pixel deduplication, custom hashing service if needed, monitoring dashboards, and monthly diagnostic audits. Typical setup is 8-12 hours of agency work versus 4-8 weeks of in-house engineering ramp-up.
Our Curly Hair Brand UK case study documents how proper Meta CAPI and Enhanced Conversions setup contributed to 15.25x Shopify ROAS, and our Pet Accessories case study shows $346K in revenue at 5.12x ROAS on the same tracking architecture. Both used the patterns covered in this guide. We’re a Google Partner and Meta Business Partner with $780M in trackable client revenue across 2,500+ ecommerce brands since 2013.
If you want to skip the multi-week DIY path, contact us for a free conversion tracking audit. We’ll measure your current match quality on both platforms, identify the gaps, and quote a flat-rate deployment if it makes sense for your account.
Conclusion
Google Enhanced Conversions and Meta CAPI compound the value of every other server-side tracking investment because they’re the layer that decides whether Smart Bidding and Advantage+ get usable signal or noise. Get the SHA-256 hashing right, get the PII normalization right, get the event_id deduplication right, and your match quality climbs to 8+ within two weeks. Skip any of those steps and you’ll be in the 35-50% match band that most accounts sit in without ever realizing they’re leaving 15-30% of ROAS on the table.
If you haven’t built the foundation yet, start with our Shopify server side tracking setup for the Custom Pixel pattern, then layer on the right server side GTM hosting for your traffic, and use this guide for the Enhanced Conversions and Meta CAPI configuration on top. For Performance Max accounts specifically, our PMax checklist covers the conversion signals that matter most for sustained scale.
Frequently asked questions about Google Enhanced Conversions and Meta CAPI
What is the difference between Google Enhanced Conversions and Meta CAPI?
Google Enhanced Conversions sends hashed customer data to Google Ads for matching against logged-in Google accounts. Meta CAPI sends the same kind of hashed data to Meta for matching against Facebook and Instagram accounts. Both use SHA-256 hashing of normalized PII; the differences are endpoint URLs, authentication, and which address fields stay unhashed.
Do Google Enhanced Conversions and Meta CAPI replace the browser pixel?
Meta CAPI complements rather than replaces the browser Pixel for best results, using event_id deduplication. Google Enhanced Conversions layers on top of standard Google tag conversion tracking and is not a replacement. Running both browser and server signals gives the highest match quality on both platforms.
How long does it take to see Google Enhanced Conversions match data?
Google Enhanced Conversions diagnostic data populates 48-72 hours after the first events are received. Meaningful match rate trends require 7-14 days of consistent data. Don’t make optimization decisions based on the first three days of data.
What is a good Event Match Quality score for Meta CAPI?
A good Event Match Quality score for Meta CAPI is 8 or higher on purchase events. Most stores land at 6-7 after basic setup and climb to 8-9 with email plus phone plus fbp cookie plus fbc cookie plus client IP. Above 9 requires sending name and address fields too.
Can I send Google Enhanced Conversions without a browser tag?
Yes, you can send Google Enhanced Conversions purely via the Google Ads API or via Data Manager uploads, with no browser tag involvement. The pattern works well for CRM-driven conversion tracking where the sale closes offline.
What does the April 2026 Google Enhanced Conversions update change?
The April 2026 update merged Enhanced Conversions for Web and Enhanced Conversions for Leads into a single unified on-off setting in Google Ads. Existing accounts were automatically migrated. Setup guides that describe two separate flows are now obsolete.
How do I generate a Meta CAPI access token?
Generate a Meta CAPI access token in Events Manager, Data Sources, your Pixel, Settings tab, Conversions API section. Click Generate access token. Treat it like a password and store in an environment variable, never in source code.
What happens if I send the same conversion twice to Meta CAPI?
If you send the same conversion twice with the same event_id, Meta deduplicates and counts it once. If you send the same conversion with different event_ids, Meta counts both as separate conversions and double-counts. This is why event_id consistency between browser Pixel and server CAPI matters.
Do I need to hash data before sending to Meta CAPI?
You need to hash data before sending to Meta CAPI only when using the direct API approach. If you use Meta’s GTM Server template or the browser Pixel, the templates hash internally and you send raw values. Never hash twice.
Why is my Google Enhanced Conversions match rate so low?
The most common reasons for low Google Enhanced Conversions match rate are: missing email or phone, incorrect SHA-256 normalization (uppercase, whitespace, missing country code), hashing already-hashed values, or hashing address fields that Google expects unhashed. The hashing utility in this guide handles all of these.
Can Meta CAPI work without the browser Pixel?
Yes, Meta CAPI can work standalone without the browser Pixel, called Conversions API Gateway. The trade-off is that Event Match Quality usually caps at 6-7 because the server can’t access fbp and fbc browser cookies. Pixel plus CAPI with event_id dedup is the recommended pattern for ecommerce.
What is the difference between fbp and fbc in Meta CAPI?
The fbp cookie is the Facebook browser ID, set on every page view, used to identify return visitors. The fbc cookie is the Facebook click ID, set when a user clicks a Meta ad with fbclid in the URL, used to attribute conversions to specific ad clicks. Both improve Event Match Quality when sent server-side.
How do I send offline conversions to Google Enhanced Conversions for Leads?
To send offline conversions to Google Enhanced Conversions for Leads, capture the gclid when the lead form is submitted, store it in your CRM with the lead record, and upload the conversion via Google Ads API uploadClickConversions when the lead closes. The hashed user data plus the original gclid identifies the conversion to Google.
Does Google Enhanced Conversions work for B2B lead generation?
Yes, Google Enhanced Conversions for Leads is specifically designed for B2B and lead generation businesses with offline conversion paths. It significantly improves Smart Bidding optimization when sales close days or weeks after the initial lead form fill.









