
Make confident, data-driven decisions with actionable ad spend insights.
© 2026 DataCops. All rights reserved.
15 min read
What’s wild is how invisible it all is. You look at your ad platform dashboard and see 100 conversions. You look at your CRM and see 80 actual sales. You have a 20% discrepancy, but the dashboard is screaming success. The revenue figures look good, the headlines are positive, and almost nobody questions the most insidious data gap of all: duplicate conversion counting. We accept the reported numbers, but often, a significant portion of those "conversions" are phantom events, counting the same customer action multiple times.

Orla Gallagher
PPC & Paid Social Expert
Last Updated
December 13, 2025
The Problem: Google Ads reports 500 conversions last month worth $100,000 revenue. Your actual sales are 350 orders worth $70,000. Google Ads counts 43% more conversions than reality. Your reported CPA is $40 but true CPA is $57. You scale campaigns based on false metrics.
The Reason: Users reload thank you pages, causing conversion tags to fire multiple times. You run both client-side pixels and server-side Conversion API without Event ID deduplication. Webhooks from your CRM retry failed sends, triggering duplicate conversion uploads. Back button navigation re-fires conversion tags. Each creates multiple conversion records for single purchases.
The Solution: Generate unique transaction IDs on your server for each order. Pass transaction ID to both client-side conversion tags and server-side Conversion API payloads. Google Ads and Meta use transaction ID to deduplicate, counting each order only once regardless of how many times reported. Implement server-side logging to prevent webhook retries from creating duplicates.
Duplicate conversions occur when the same purchase or action gets counted multiple times in advertising platforms, inflating conversion counts and distorting cost and ROI metrics.
How duplicates happen:
Single customer completes one purchase for $100.
Conversion tag fires when they land on thank you page (conversion #1 reported).
Customer reloads page, tag fires again (conversion #2 reported).
Platform counts 2 conversions worth $200 total.
Reality: 1 customer, 1 order, $100 revenue.
Platform reports: 2 conversions, $200 revenue.
The business impact:
Your true cost per acquisition is $50.
Platform reports CPA as $25 (doubled conversions).
You increase budgets thinking campaigns are 50% more profitable than reality.
Actual profitability decreases while reported metrics improve.
Duplicate conversions occur at four points: page reloads, simultaneous pixel and API sends, webhook retries, and browser navigation.
Most common source: user refreshes thank you page after purchase.
The scenario:
Customer completes checkout, lands on order confirmation page.
Conversion tag fires via page view trigger (conversion #1).
Customer hits F5 to reload page (wants to screenshot receipt).
Page view trigger fires again (conversion #2).
Same order counted twice.
Why standard prevention fails:
GTM cookie-based prevention: User in incognito mode has no persistent cookies.
Client-side checks: Safari ITP deletes cookies within 7 days, check fails on return visits.
Variable flags: Browser cache refresh resets JavaScript variables.
The statistics:
5-15% of customers reload thank you pages.
Without deduplication, this creates 5-15% conversion inflation.
For $100,000 monthly ad spend at $50 CPA, this is $5,000-$15,000 wasted on false metrics.
Running both client-side pixel and server-side Conversion API without coordination creates duplicates.
The double-send scenario:
Customer completes purchase.
Client-side Google Ads tag fires immediately (send #1 to Google).
Your backend also sends purchase to Conversion API (send #2 to Google).
Both sends lack Event ID for deduplication.
Google counts 2 separate conversions for 1 purchase.
Why this is common:
Marketers add Conversion API for reliability (bypasses ad blockers).
Forget to remove or coordinate with existing pixel.
Both systems operate independently.
Each thinks it's the only source of truth.
The transition trap:
Month 1: Only pixel running, 400 conversions reported.
Month 2: Add Conversion API, keep pixel, 800 conversions reported (doubled).
No actual business growth, just duplicate reporting.
CRM and payment platforms send webhooks to notify your system of completed transactions. Failed deliveries trigger retries.
The retry scenario:
Stripe sends "payment_success" webhook to your server.
Your server processes webhook, sends conversion to Google Ads API.
Network timeout prevents Stripe from receiving confirmation.
Stripe assumes delivery failed, retries webhook 30 seconds later.
Your server processes "new" webhook, sends duplicate conversion.
Common retry patterns:
Shopify: Retries up to 3 times for failed webhooks.
Stripe: Retries up to 5 times over 3 days.
Custom CRMs: Often retry indefinitely until success confirmation.
Single transaction can trigger 3-5 duplicate conversion sends.
Back button and forward button navigation can re-trigger conversion tags.
The navigation scenario:
Customer completes purchase, lands on /thank-you page.
Conversion tag fires (conversion #1).
Customer clicks back button (returns to checkout).
Customer clicks forward button (returns to /thank-you).
Conversion tag fires again (conversion #2).
Why this happens:
Browser cache serves previous page view.
Tag fires on cached page load.
No memory that tag already fired for this transaction.
Event ID deduplication uses unique transaction identifiers to ensure platforms count each conversion only once.
How Event ID works:
Step 1: Server generates unique ID
Customer completes purchase, order ID: ORDER-12345.
Server generates unique Event ID: TXN-abc123xyz789.
Step 2: Client-side tag includes Event ID
Server passes Event ID to webpage data layer.
Conversion tag retrieves Event ID from data layer.
Tag fires with Event ID parameter:
transaction_id: TXN-abc123xyz789
value: 100.00
currency: USD
Step 3: Server-side API includes same Event ID
Backend sends conversion to API:
{
"transaction_id": "TXN-abc123xyz789",
"value": 100.00,
"currency": "USD"
}
Step 4: Platform deduplicates
Google Ads receives client-side conversion with transaction_id TXN-abc123xyz789.
Later receives API conversion with same transaction_id TXN-abc123xyz789.
Recognizes duplicate, counts only 1 conversion.
What makes good Event IDs:
Unique per transaction (not per user or session).
Generated server-side (not client-side random numbers).
Persisted in database for future reference.
Same format used across all systems (client tags, API calls, CRM).
Transaction IDs must be created server-side when order completes.
Server-side generation (correct):
Customer submits payment.
Payment processor confirms success.
Server creates order record in database with unique Order ID.
Server generates transaction ID based on order: TXN-{ORDER_ID}-{TIMESTAMP}.
Example: TXN-ORD12345-1701456789
Client-side generation (incorrect):
JavaScript creates random ID when thank you page loads: Math.random().toString().
Problems:
Page reload creates different random ID (defeats deduplication)
Server has no record of this ID (cannot use for API sends)
Cannot match to actual order in database
Best practice format:
Use UUID v4: f47ac10b-58cc-4372-a567-0e02b2c3d479
Or combine order ID + timestamp: ORDER-12345-20241201143000
Must be deterministic (same order always generates same ID, not random).
Server must inject transaction ID into page so conversion tags can include it.
Method 1: Data layer push (recommended)
Server renders thank you page with JavaScript:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'purchase',
'transaction_id': 'TXN-ORD12345-1701456789',
'value': 100.00,
'currency': 'USD'
});
</script>
GTM conversion tag retrieves transaction_id from data layer variable.
Method 2: Hidden form field
Server renders hidden input:
<input type="hidden" id="transaction_id" value="TXN-ORD12345-1701456789">
JavaScript reads value and passes to conversion tag.
Method 3: URL parameter
Server redirects to: /thank-you?tid=TXN-ORD12345-1701456789
JavaScript reads URL parameter and passes to tag.
Common mistakes:
Server generates ID but forgets to pass to page (tag sends no ID).
ID passed but GTM variable misconfigured (tag sends "undefined").
ID format changes between server and client (server uses ORDER-123, client expects TXN-123, no match).
Google Ads uses transaction_id parameter for deduplication.
In Google Ads conversion tag:
Tag Type: Google Ads Conversion Tracking
Conversion ID: AW-123456789
Conversion Label: abcDEFghiJKL
Add transaction ID parameter:
Click "Advanced Settings"
Add parameter:
Parameter: transaction_id
Value: {{Transaction ID Variable}}
{{Transaction ID Variable}} must be GTM variable that reads from data layer.
Testing:
Fire test conversion with transaction_id: TEST-123.
Send same conversion again with same transaction_id: TEST-123.
Check Google Ads (wait 3-6 hours for processing).
Should show 1 conversion, not 2.
Meta (Facebook) uses event_id parameter for deduplication.
In Meta Pixel:
fbq('track', 'Purchase', {
value: 100.00,
currency: 'USD'
}, {
eventID: 'TXN-ORD12345-1701456789'
});
Event ID passed as third parameter in fbq track call.
In Meta Conversions API:
{
"data": [{
"event_name": "Purchase",
"event_time": 1701456789,
"event_id": "TXN-ORD12345-1701456789",
"user_data": {
"em": "hashed_email"
},
"custom_data": {
"value": 100.00,
"currency": "USD"
}
}]
}
Deduplication logic:
Pixel fires with event_id: TXN-ORD12345-1701456789.
API sends same event_id: TXN-ORD12345-1701456789 (within 48 hours).
Meta recognizes duplicate event_id, counts 1 purchase.
If API send happens >48 hours after pixel, both may count (Meta's deduplication window).
Webhook retry duplicates require server-side logging to detect repeated sends.
Webhook deduplication flow:
Step 1: Maintain sent conversions log
Database table: conversion_api_log
Columns: transaction_id, sent_at, platform, response_code
Step 2: Webhook arrives
Shopify sends webhook: order_id: 12345, transaction_id: TXN-ORD12345-1701456789.
Step 3: Check if already sent
Query database:
SELECT * FROM conversion_api_log
WHERE transaction_id = 'TXN-ORD12345-1701456789'
AND platform = 'google_ads'
Step 4a: Not found, send conversion
No record found, this is first send.
Send to Google Ads Conversion API.
Log entry:
transaction_id: TXN-ORD12345-1701456789
sent_at: 2024-12-01 14:30:00
platform: google_ads
response_code: 200
Step 4b: Found, skip duplicate
Record exists with response_code: 200.
Skip API call entirely (already successfully sent).
Log: "Duplicate webhook detected for TXN-ORD12345-1701456789, skipped."
Retry handling for failed sends:
If original send got response_code: 500 (server error), allow retry.
If response_code: 200 (success), block all retries.
This ensures legitimate failed sends can retry without creating duplicates for successful sends.
Prevent page reload duplicates with server-side Event ID (not client-side cookies).
Why cookies fail:
Page reload: Cookie exists, tag checks cookie, skips duplicate (works).
User incognito: No cookies persist, tag cannot detect previous fire (fails).
Safari ITP: Cookie expires in 7 days, returning user's tag fires again (fails).
Why Event ID works:
Page reload: Tag fires with same transaction_id from data layer.
Platform already received this transaction_id, deduplicates automatically.
Works in incognito, works on Safari, works on return visits.
Implementation:
Don't rely on client-side "cookie check" logic.
Always pass transaction_id from server to page.
Always include transaction_id in tag payload.
Let platform handle deduplication (not browser logic).
Mistake 1: Different IDs for pixel vs API
Client-side tag uses: ORDER-12345
Server-side API uses: TXN-ORD-12345-20241201
IDs don't match, platform counts as 2 separate conversions.
Fix: Use identical transaction_id format across all systems.
Mistake 2: No transaction ID for Conversion API
Pixel includes transaction_id correctly.
Conversion API payload omits transaction_id field.
Platform cannot deduplicate, counts both.
Fix: Always include transaction_id in API payloads.
Mistake 3: Transaction ID not in database
Server generates ID for client tag but doesn't save it.
Days later, need to send Conversion API for same order.
No record of original ID, generate new ID.
Platform sees different IDs, counts as 2 conversions.
Fix: Save transaction_id to database when generated, query for future API sends.
Mistake 4: Client generates random ID
JavaScript creates transaction_id with Math.random().
Page reload creates different random ID each time.
Multiple IDs for same order, deduplication fails.
Fix: Server generates deterministic ID (UUID or order-based), passes to page.
Mistake 5: Forget Event ID for Meta
Google Ads implementation uses transaction_id correctly.
Meta Pixel and CAPI don't include event_id parameter.
Meta counts duplicates while Google deduplicates.
Fix: Implement event_id for Meta using same transaction_id value.
Server-side generation:
[ ] Transaction ID generated server-side (not client JavaScript)
[ ] ID uses UUID format or order ID + timestamp
[ ] ID saved to database with order record
[ ] Same ID accessible for future API calls
Client-side implementation:
[ ] Server passes transaction ID to data layer on thank you page
[ ] GTM variable correctly retrieves ID from data layer
[ ] Google Ads tag includes transaction_id parameter
[ ] Meta Pixel includes eventID parameter
[ ] Test: Data layer shows transaction_id value in GTM Preview Mode
Server-side API:
[ ] Conversion API payload includes transaction_id (Google) or event_id (Meta)
[ ] ID matches format used in client-side tags
[ ] Database query retrieves correct ID for each order
[ ] API response logged with transaction_id for audit trail
Deduplication testing:
[ ] Fire client tag with test transaction_id: TEST-001
[ ] Send API conversion with same transaction_id: TEST-001
[ ] Wait 6-24 hours for platform processing
[ ] Verify platform reports 1 conversion (not 2)
[ ] Test page reload: reload thank you page, verify still 1 conversion
Webhook protection:
[ ] Database table logs sent conversions with transaction_id
[ ] Webhook handler checks log before sending API call
[ ] Successful sends (200 OK) blocked from retry
[ ] Failed sends (500 error) allowed to retry
Example scenario:
Monthly ad spend: $50,000
Actual conversions: 500
Reported conversions with duplicates: 750 (50% inflation)
Reported metrics (false):
CPA: $50,000 ÷ 750 = $66.67
Appears profitable, you increase budget.
True metrics:
CPA: $50,000 ÷ 500 = $100
Actually 50% more expensive than reported.
Decision impact:
Based on false $66.67 CPA: Increase budget to $75,000.
True CPA is $100: Budget increase loses money.
Duplicate conversions cause $25,000 budget misallocation.
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What causes duplicate conversions?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Duplicate conversions are caused by users reloading thank you pages, running both client-side pixels and server-side Conversion API without Event ID deduplication, webhook retry attempts, and browser back/forward button navigation that re-triggers conversion tags."
}
},
{
"@type": "Question",
"name": "How do I prevent duplicate conversions in Google Ads?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Prevent duplicate conversions by generating unique transaction IDs on your server, passing them to both client-side conversion tags and server-side Conversion API using the transaction_id parameter, allowing Google Ads to deduplicate events automatically."
}
},
{
"@type": "Question",
"name": "What is Event ID deduplication?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Event ID deduplication uses unique transaction identifiers passed in both client-side tags and server-side API calls. Platforms like Google Ads and Meta recognize matching Event IDs and count each transaction only once even if reported multiple times."
}
},
{
"@type": "Question",
"name": "How do I test if deduplication is working?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Test deduplication by sending a conversion with a test transaction ID from both your pixel and Conversion API, waiting 6-24 hours, then verifying the platform reports only 1 conversion instead of 2 for the duplicate transaction ID."
}
}
]
}
DataCops is a first-party data platform that prevents duplicate conversions through centralized Event ID management, coordinated client-side and server-side sends, and webhook deduplication logging.
How DataCops prevents duplicates:
Unified Event ID generation:
Platform generates unique transaction IDs server-side when conversions occur.
Same ID used across all systems: client tags, Conversion API, CRM records.
No client-side random IDs or format inconsistencies.
Transaction IDs persist in DataCops database for future reference.
Coordinated pixel and API sends:
DataCops acts as single collection point for conversion data.
Client-side script captures conversion, sends to DataCops server with Event ID.
Server validates data, removes duplicates, then distributes to platforms.
Google Ads and Meta receive single, deduplicated stream with consistent Event IDs.
Eliminates pixel vs API double-counting from uncoordinated systems.
Webhook deduplication logging:
Built-in conversion API log tracks all sent events by transaction_id.
When CRM webhook arrives, DataCops checks log first.
If transaction_id already sent with 200 OK response, skips duplicate send.
Protects against Shopify, Stripe, and custom CRM webhook retries.
Page reload protection:
Server-side Event ID passed to client remains constant on page reloads.
Client tag fires multiple times but with same transaction_id.
Platform deduplicates automatically, no client-side cookie logic needed.
Works in incognito mode and survives Safari ITP restrictions.
Bot filtering integration:
Real-time bot detection prevents fake conversions from receiving Event IDs.
Bot traffic blocked before Event ID assignment.
Ensures duplicate prevention only applied to legitimate human transactions.
Multi-platform consistency:
Identical Event ID format sent to Google Ads (transaction_id), Meta (event_id), Microsoft Ads, LinkedIn.
All platforms deduplicate using same source of truth.
No platform-specific ID mismatches.
Audit trail and verification:
Complete log of every conversion sent with timestamp, platform, Event ID, response code.
Monthly reports show:
Total conversions by platform
Duplicate sends detected and prevented
Event ID match rates across systems
Verification dashboard compares client-side fires vs server-side API sends vs platform reported conversions.
Implementation timeline:
Week 1: Event ID generation logic and database schema
Week 2: Client-side tag configuration with Event ID variables
Week 3: Conversion API integration with deduplication logging
Week 4: Webhook handler setup and retry protection
Total deployment: 4 weeks from start to fully deduplicated conversion tracking.
Platform automatically handles ongoing Event ID management and deduplication with no manual intervention required.
Supported platforms for deduplication:
Google Ads (transaction_id parameter)
Meta (event_id for pixel and Conversions API)
Microsoft Ads
LinkedIn Ads
TikTok Ads
Custom conversion endpoints
Key Takeaways:
Duplicate conversions inflate reported conversion counts by 30-50% on average, making CPA appear lower than reality
Generate unique transaction IDs server-side for each order, not client-side random numbers
Pass same transaction ID to both client-side conversion tags and server-side Conversion API
Use transaction_id parameter for Google Ads and event_id for Meta to enable platform deduplication
Implement webhook deduplication logging to prevent retry attempts from creating duplicates
Test deduplication by sending same transaction ID twice and verifying platform counts only 1 conversion
Page reload protection requires server-side Event ID, not client-side cookie checks that fail in incognito mode
Save transaction IDs to database for future API calls to maintain consistency across delayed sends