A browser Purchase and a server Purchase can describe the same order. The integration needs a shared event identity so receiving both copies does not turn one action into two reported events.
This guide follows that identity through the browser event, GTM forwarding, and the server payload. A report percentage or a rising ROAS is a reason to inspect the setup, rather than proof of the cause.

What duplicate CAPI events look like (and why they happen)
Take a completed test order and trace every source that sends its Purchase event. Capture the event name, dataset or pixel, identifier, value, currency, event time, and transport result. Browser and server copies should describe the same action consistently.
A duplicate can come from different identifiers, repeated listeners, retries that create new identities, or an additional integration sending its own purchase. The report total does not distinguish those causes. Inspect the payloads and the integration inventory before changing the implementation.
How Meta event_id deduplication actually works
Use the same event name and shared identifier for the browser and server copies sent to the same destination. The browser Pixel API calls its option eventID; the CAPI payload calls the field event_id. A template may label either field Event ID, so verify the emitted request rather than the label alone.
const purchaseEventId = "purchase_" + String(order.id);
fbq("track", "Purchase", { value: order.value, currency: order.currency }, {
eventID: purchaseEventId,
});
const serverEvent = {
event_name: "Purchase",
event_id: purchaseEventId,
// Include the required time, source, user data, and order values.
};
Here, order is data supplied by the supported checkout integration. The example shows the identity contract, rather than a complete checkout adapter. Keep that identity stable through retries and generate a different identity for a genuinely different occurrence.
The older articles described a forty eight hour period as an automatic correction of broken reports. That conclusion was not established. A matching window is different from a guarantee that mismatched historical events will repair themselves. Meta's primary page could not be retrieved during this review, so the guide does not publish a numeric correction deadline or a universal deduplication percentage as a pass condition.
Check the native integration before adding another sender
A standard Shopify checkout with one correctly configured native Meta integration can already provide browser and server events. Inspect a test order before replacing it or adding a GTM overlay. A custom checkout flow, extra purchase listener, or another integration can introduce a separate event identity outside the native path.
Build the source inventory from the Facebook and Instagram channel, customer event pixels, GTM web tags, GTM server tags, backend jobs, and enabled marketing app integrations. For each Purchase sender, record the owner, trigger, destination, identifier construction, and what happens on a retry. Klaviyo or an attribution tool can have an event sending feature enabled, but installation alone does not prove it sends duplicate events.
Assign one intentional purchase identity across the copies you keep. Disable or correct a redundant sender only after confirming its job and checking the effect on consent and the remaining event path.
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: 'purchase_' + String(order.id), // supplied by the checkout adapter
// ...rest of your purchase data
});
For a different event occurrence such as ViewContent or AddToCart, generate its identifier once at the action:
window.dataLayer.push({
event: 'view_item',
event_id: crypto.randomUUID(),
// ...
});
The purchase example assumes an order object from the supported checkout adapter. Shopify's standard customer events document checkout_completed; a custom pixel runs in its own environment, so adapt the transport there instead of pasting legacy order status Liquid into a current checkout. Send the identifier as a string and preserve it into the server request.
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
Publish to the test path first and make a known purchase. In Events Manager and the transport logs, locate the browser and server copies and compare their event name and identifier. Confirm that they reach the intended dataset and that the receiver accepts the requests. Use the platform's available test and diagnostic views without assuming that every dashboard updates immediately.
Keep the test order reference with the before and after evidence. Exercise refresh, back navigation, a network retry, a blocked browser request, and the native integration alongside any custom path. A repeated transport attempt should retain the identity for the same event; a different order should have a different identity.
Review the aggregate report after enough relevant traffic has arrived. The expected browser and server overlap depends on how the store collects events. Use known event pairs and transport evidence to establish acceptance; the earlier percentage band cannot certify the setup. Compare payload evidence, transport results, and the store's known event populations.
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.
Arrival order and delays. Preserve the same identity regardless of which copy arrives first. Inspect actual receipt and transport evidence when a copy is delayed, and check the current receiver documentation before setting any delivery deadline.

What to check next after fixing dedup
After verifying event identity and delivery, inspect the matching identifiers your integration is permitted to send, including external_id. Use a stable customer identifier when available, apply the receiver's required normalization and hashing, and preserve consent. A visitor who has signed in may have a customer identifier available on an earlier event as well as 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 Tracking Gap is the original implementation case study. Treat its recorded timing and score as that project's account.

How long does it take to see dedup results in Events Manager after making changes?
Test the known event copies first, then allow the platform report to reflect the new traffic. The source account's timing observation does not establish a guaranteed correction period for another setup.
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%?
Determine the expected overlap from the actual event sources. Some actions may reach only one transport. A percentage alone cannot establish whether the shared identities and delivery are correct.
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 combined guide retains the browser data layer, GTM variable mapping, server forwarding, and verification workflow. Its source inventory and native integration exception come from the original double counting article.
See Shopify standard customer events for supported checkout events and Meta's deduplication reference for current receiver behavior. The Meta page returned a retrieval limit during the October 23, 2026 review. Earlier claims of a universal 85 to 95 percent pass band and automatic forty eight hour correction were not supported by the evidence retained for this guide, so neither is used as a pass criterion.