Skip to content
← ALL WRITING

2026-04-21 / 12 MIN READ

How to Fix Shopify Pixel and CAPI Duplicate Events

Step-by-step guide to fixing duplicate Meta conversions from Shopify Pixel and CAPI using event_id deduplication. Includes GTM setup and verification steps.

The Events Manager deduplication tab is showing less than 85%. Your Shopify Pixel and your CAPI server tag are both firing for the same purchases, and Meta is logging two conversions for every real one. For about 48 hours your ROAS looks elevated. Then Meta's own deduplication runs, collapses the doubles, and your reported revenue drops without explanation.

The fix is a shared event_id that matches exactly on both sides before either event is sent. This guide walks through wiring that in GTM from scratch, including how to verify it's working in Events Manager.

sequence diagramevent_id deduplication flow
Browser
Pixel
Meta
Events API
CAPI
Server
Purchase
Purchase
ord_8b4d2f
Purchase
Purchase
ord_8b4d2f
event_id match detected
Purchase - 1 event
ord_8b4d2f - counted once
waiting
Browser Pixel and CAPI Server both fire a Purchase event. When both carry the same event_id, Meta merges them into one conversion - the deduplication mechanism this guide configures.

What duplicate CAPI events look like (and why they happen)

Open Events Manager, go to your pixel's Events tab, and click on a Purchase event. There is a Deduplication section. If your setup is broken, it will show either 0% (events received from both sources but nothing is matching) or you will see double the expected event volume with a dedup rate below 85%.

The mechanics are simple: your browser fires the Meta Pixel the moment a customer hits the thank-you page. Simultaneously, your server-side GTM container sends a CAPI event for the same purchase. Meta receives two separate events. If they carry different event_id values, Meta counts them as two conversions.

This inflates your numbers in two bad ways. Short-term, your ROAS looks inflated - Meta Ads Manager shows more conversions than actually happened. Within 48 hours, Meta's own dedup catches it, collapses the doubles, and your reported ROAS drops. Your ad account's learning phase gets confused because the optimization signal keeps shifting under it. The underlying cause of this specific gap - iOS 14.5 and ATT breaking browser-only measurement - is worth understanding too; why iOS 17 ATT breaks pixel tracking explains the client-side failure that makes CAPI necessary in the first place.

I built a full server-side CAPI stack for an ecommerce brand that had been running browser-only Pixel for 18 months. Once both Pixel and CAPI were live, deduplication was the first thing that had to be correct. Running both without a shared event_id is worse than running either one cleanly on its own.

How Meta event_id deduplication actually works

Meta compares every incoming event against others received from the same Pixel ID within a 48-hour window. If two events share the same event_id string and the same event name (e.g., Purchase), Meta counts only one.

Meta holds the deduplication window for 48 hours, giving you time to align both the client and server sides.

Shopify's native CAPI integration does generate an event_id, but only for events it handles directly. If you have a GTM web container also firing pixel events, those events get a separate, GTM-generated ID. Neither system knows about the other's ID, so dedup never fires.

Prerequisites

Before starting:

  • A Shopify store with Meta Pixel firing via GTM web container (or both GTM and Shopify native)
  • A GTM server container, hosted on Stape or your own server container URL
  • A Meta CAPI tag active and receiving traffic in the server container
  • Access to Events Manager (you need the Deduplication tab)
  • Optional but useful: a Stape account with the event deduplication report enabled

If you do not have a server container yet, set that up first. The dedup fix only matters once both legs are firing. If you are not sure whether your overall CAPI setup is ready for this step, the CAPI implementation readiness checklist is worth reading before you start.

Step 1 - Generate a shared event_id in your GTM web container

The event_id needs to be created once per event and shared to both the Pixel tag and the server container. The web container generates it; the server container reads it.

Push the event_id to the dataLayer before any tags fire. For purchase events, use the Shopify order ID because it is stable and already unique:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'purchase',
  event_id: '{{ order.id }}',  // Shopify Liquid syntax in the thank-you template
  // ...rest of your purchase data
});

For non-purchase events like ViewContent or AddToCart, generate a per-session ID:

window.dataLayer.push({
  event: 'view_item',
  event_id: Date.now().toString() + Math.random().toString(36).slice(2),
  // ...
});

The toString() calls matter. Always push a string, not a number.

In GTM, create a Data Layer Variable named DLV - event_id that reads event_id. Set its data layer version to Version 2. This variable will be used in both your Pixel tag and your forwarded request to the server container.

Step 2 - Map event_id into your Meta Pixel tag

Open your Meta Pixel tag in the GTM web container.

If you are using the Meta Pixel base code tag, go to the tag's additional parameters or custom fields section and add:

Field name: event_id
Value: {{DLV - event_id}}

If you are using a community Meta Pixel template, look for an "Event ID" field - most templates surface it explicitly.

Test with the Meta Pixel Helper Chrome extension. Fire a purchase event on your test store's thank-you page and confirm the event_id field shows a non-empty string in the Pixel Helper output. Write that value down - you will use it in the next step to verify the server side is sending the same one.

Step 3 - Pass the same event_id through the server container

Your server container receives data from the web container via the GA4 client or a custom client. The event_id you pushed to the dataLayer travels in the request, usually as a custom parameter.

In your server container, create a variable to read the incoming event_id. If you are using the GA4 client, the event_id will arrive in the event_parameters object. The exact path depends on how your web-to-server forwarding is configured, but it typically looks like:

Variable type: Event Data
Key path: event_id

Open your Meta CAPI tag in the server container. Find the Event ID field (it is a standard field in all Meta CAPI templates). Set it to your server-side event_id variable.

Type consistency check: make sure the server variable returns a string, not a number. If your order ID comes in as an integer from Shopify's dataLayer and you did not call toString() in Step 1, it will arrive as a number and the dedup will fail silently. The fix is always in Step 1 - push a string.

Step 4 - Verify in Events Manager

Wait 20-30 minutes after making changes. GTM preview mode is not sufficient for verifying dedup because you need actual traffic from two sources.

In Events Manager:

  1. Select your Pixel
  2. Go to Events
  3. Click on Purchase (or whichever event you are testing)
  4. Scroll to the Deduplication section

A healthy dedup rate is 85-95%. This means Meta received both events but merged them into one count for 85-95% of cases. Some events will legitimately arrive from only one source (a purchase completed on a device with an ad blocker, for example), so 100% is not the target.

If you see 0%: both events are arriving but the event_id values are not matching. Go back to Step 1 and verify the string you pushed is exactly what the server container is forwarding. Use a custom field in your CAPI tag to log the event_id to your server container logs - Stape's event testing interface shows you the raw payload.

If you see above 95%: your server events may be getting rejected because they arrive after the 48-hour window closes, or there is a configuration issue causing the CAPI tag to suppress events when it finds a match. Check the server container's response codes in Stape's event logs.

Common mistakes that break Shopify CAPI deduplication

Type mismatch. Shopify order IDs can arrive as integers depending on how your dataLayer push is templated. Always add .toString() when building the event_id. Meta's system is not forgiving about this.

Third-party tools firing their own CAPI tags. If you have Klaviyo's Meta integration active, or Triple Whale, or Northbeam, they may be sending their own CAPI events with their own event_ids. You will see this as event volume that does not match your GTM data. Check Events Manager's event sources - if you see multiple sources for the same event type, you have duplicate senders, not just duplicate dedup failures. Klaviyo's behavior here is particularly worth checking - how Klaviyo fires before consent covers what that looks like and how to isolate it.

event_source_url encoding. This does not directly break dedup, but it does affect match quality and can cause confusion in Events Manager. Send the canonical URL without query parameters for standard events, or match whatever the Pixel is sending.

Server event arriving before client event. Rare, but possible on fast server setups with slow client connections. If a Stape container responds faster than the user's browser loads the thank-you page, the server event arrives first and sets the anchor. This is usually fine - Meta holds the window for 48 hours - but it can cause unusual dedup patterns in the early hours after launch.

What to check next after fixing dedup

Once dedup is running at 85-95%, the next highest-leverage fix is external_id. This is a SHA-256 hashed version of the Shopify customer ID, sent on every authenticated event (ViewContent for logged-in users, AddToCart, InitiateCheckout, Purchase).

Adding external_id on its own moved match quality from 3.8 to 6.2 in one implementation I ran for a Shopify DTC brand - before even touching email or phone hashing. Meta can now link events across devices for that customer, which the pixel alone cannot do.

For the full picture of what breaks a CAPI setup - match quality, event coverage, PII hashing, checkout extensibility - I wrote a separate rundown of the six most common misconfigurations in a CAPI audit.

If you want to run the full 14-check diagnostic on your own store and get a prioritized fix list with dollar-impact estimates, that is what the CAPI Leak Report covers. The same methodology I used when rebuilding CAPI from scratch for a Shopify brand is what runs the audit.

The full production case study - building the entire server-side infrastructure from scratch in 48 hours, including the dedup architecture described here - is at The Tracking Gap.

How long does it take to see dedup results in Events Manager after making changes?

Allow 20-30 minutes for live traffic to accumulate. Events Manager's deduplication report updates on a rolling basis, not in real time. If you are testing with a real purchase on a staging or test store, the event_id should show up in the CAPI tag's test event viewer almost immediately, but the dedup rate in the dashboard takes a few minutes to calculate.

Does Shopify's native CAPI integration handle event_id deduplication automatically?

Shopify's native integration generates its own event_ids for events it handles. If you are using only Shopify's native integration and have turned off all GTM pixel tags, it handles dedup for those events. The problem is when you have both running - Shopify native and a GTM web container. They generate separate IDs, and you get doubles. The GTM approach in this guide gives you one source of truth.

What is a normal dedup rate and should I aim for 100%?

85-95% is the healthy range. You will always have some events that arrive from only one source - ad-blocked sessions, server-only iOS events, etc. - and those do not need to be deduped because there is nothing to dedup. Aiming for 100% usually means you have suppressed the client-side pixel, which defeats the redundancy the setup is supposed to provide.

Can I use a UUID library to generate event_id instead of the order ID for purchases?

You can, but it is unnecessary for purchase events and introduces a small risk of mismatch if the UUID is generated in multiple places. For Purchase, the Shopify order ID is already unique and stable. For upper-funnel events (ViewContent, AddToCart), a timestamp plus random string is fine. The order ID approach also makes it easier to trace specific events when debugging, because you can match the event_id to a real order in your Shopify admin.

What happens if my event_id is the same across different event types?

Meta's dedup matches on event_id AND event name together. So using the same event_id string for a ViewContent and a Purchase would not cause a problem - they are different event names. But within the same event type, event_ids must be unique per event occurrence. If you accidentally reuse the same ID for two separate purchase events (e.g., two different orders both sending event_id "store_purchase"), only one will be counted.

Sources and specifics

  • The deduplication rate target of 85-95% is from Meta's own Events Manager guidance; rates outside this window indicate either over-dedup (server rejection) or under-dedup (double-counting).
  • The match quality improvement from 3.8 to 6.2 via external_id is from a production Shopify DTC implementation, Q2 2024.
  • The 48-hour deduplication window is Meta's documented behavior for the Conversions API.
  • All GTM configuration steps apply to GTM web container version 3+ and GTM server container 2024 builds.
  • Stape was used as the server container host in the production implementation referenced here.

// related

DTC Stack Audit

If this resonated, the audit covers your tracking layer end-to-end. Server-side CAPI, dedup logic, and attribution gaps - all mapped to your stack.

>See what is covered