Tracking & Attribution

What Should the event_id Be for Meta Deduplication?

The event_id for Meta deduplication must be deterministic and derived from the conversion record. Here's what to use, per event type, and what breaks.

Quick answer

The event_id should be a value both the browser and your server can compute independently from the conversion itself — an order ID, a lead record ID, or a hash of pixel ID, event name and record ID. It must be deterministic, unique per conversion, and identical on every retry. Random UUIDs, timestamps and per-pageview IDs all break deduplication, because the two halves of the same sale end up carrying different strings and Meta counts the conversion twice.

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

The event_id should be a value both the browser and your server can compute independently from the conversion itself: an order ID, a lead record ID, or a hash built from the pixel ID, the event name and that record ID. It has to be deterministic, unique per conversion, and identical on every retry. If the two halves of the same sale carry different strings, Meta counts the sale twice.

That is the whole rule, and almost every duplicate-conversion problem is a violation of it. The deduplication logic on Meta’s side is not fragile — it is doing exactly what it was told. It was told two different IDs, so it recorded two different conversions.

What does Meta actually do with the event_id?

Meta matches a browser Pixel event against a Conversions API server event when both carry the same event_name and the same event_id, and both arrive within roughly 48 hours of each other (Meta, Conversions API deduplication documentation). When that pair matches, Meta keeps one and drops the other. When it does not, both are real conversions as far as the dataset is concerned.

Two things follow from that, and they matter more than any code you will write.

First, the event name is half the key. Purchase from the Pixel and purchase from the server do not deduplicate, and neither do Lead and CompleteRegistration. The name has to be identical, including case.

Second, Meta also supports a fallback method: events carrying the same event_name plus a matching fbp or external_id can deduplicate without an event_id at all. That is genuinely useful as a safety net, but it is not a plan. It depends on a cookie surviving in the browser, which is the exact thing server-side tracking exists to stop depending on. Use event_id as the primary key and treat the fallback as insurance. The wider question of whether you need both the Pixel and the Conversions API has a similar answer: redundancy is the point, but only if the redundant events identify themselves as the same event.

What should the event_id actually be?

Derive it from the thing that happened, never from when it happened or from randomness. A good event_id has three properties: it is deterministic (the same conversion always produces the same string), unique (two different conversions never collide), and stable across retries (a redelivered webhook regenerates the identical value).

The practical shape:

Event Derive the event_id from Why
Purchase Order ID One order, one ID, already unique and already on both sides
Lead Form submission or lead record ID Survives a thank-you page reload
InitiateCheckout Checkout token or cart ID Stable while the cart lives
Subscribe Subscription ID from the billing system The browser is usually long gone
ViewContent Session ID + content ID No record exists yet; scope it to the visit

Hashing that input is optional but worth doing. Running sha256(pixel_id + "|" + event_name + "|" + record_id) gives you a fixed-length, opaque string that does not leak your internal order numbering into a third party’s system, and it guarantees the same format for every event type. The important property is not the hash — it is that both sides can recompute it from data they already hold.

Deterministic event_id flow: pixel_id, event_name and record_id hashed with SHA-256 into one event_id carried by both the browser Pixel event and the server CAPI event

One caveat on scoping: include the pixel or dataset ID in the input if you send the same conversion to more than one destination. It costs nothing and it stops a shared order ID from colliding across configs.

How do you get the same event_id into both events?

Pick one side to generate it and hand it to the other. There are two patterns that work and one that never does.

Server generates, browser receives. Your backend computes the ID when it renders the confirmation page, and passes it to the Pixel as the eventID option:

// Rendered server-side into the confirmation page
const eventId = "3f9ac1d0e7b44c81"; // sha256(pixel_id|event_name|order_id)

fbq('track', 'Purchase',
  { value: 129.00, currency: 'AUD' },
  { eventID: eventId }
);

The same string goes onto the server event as event_id. This is the more robust pattern, because the server already owns the order record and does not have to trust anything the browser sends back.

Browser generates, server receives. The tag computes the ID and posts it alongside the conversion payload. This works, but the value has to be persisted with the conversion record immediately — if it only lives in a JavaScript variable, a delayed or retried server send has nothing to regenerate from.

Both sides generate independently. This is the pattern that produces the duplicates. Two crypto.randomUUID() calls, or two timestamps, or two “unique” IDs built from different fields, will never agree. If you are chasing a live duplication problem rather than designing a new one, the Meta CAPI deduplication debug guide walks the diagnostic in order.

Why do random UUIDs and timestamps fail?

Because neither one is reproducible, and deduplication is a reproducibility problem. A random UUID is unique — which is exactly the wrong property. Uniqueness per call is the opposite of uniqueness per conversion.

Four concrete failure modes come from the same root:

The confirmation page reload. A buyer refreshes the thank-you page, or the page is reachable from their history. A per-pageview random ID fires a second Purchase with a new ID. An order-derived ID fires the same event twice and Meta keeps one.

The webhook retry. Shopify, Stripe and WooCommerce all redeliver webhooks that did not get a clean 200. If the ID is generated at send time, every redelivery is a new conversion.

The millisecond gap. Date.now() on the client and on the server are never equal, and a timestamp rounded to the second is both collision-prone and still mismatched across the boundary.

Two dispatch paths for one sale. Most real setups end up with more than one route to the same conversion — a pixel and a webhook, or a real-time path and a catch-up worker. Without a deterministic ID, each path is a separate conversion. This is the specific trap on Shopify’s Customer Events sandbox, where the sandboxed pixel and the store’s order webhook both have a legitimate claim to the same purchase.

What event_id do you use when there is no browser event at all?

Use the billing or CRM record ID, and understand that the ID’s job has changed. When a trial converts in Stripe nine days after signup, or a sales rep closes a deal in the CRM, there is no Pixel event to deduplicate against. Nothing is competing for that conversion.

The event_id is still mandatory in practice, because your own system will try to send that conversion more than once — a retry, a catch-up worker, a manual resend, a backfill someone runs twice. A deterministic ID derived from the invoice or record makes every one of those attempts idempotent.

Two constraints to design around. Meta clamps event_time to a seven-day window, so a conversion you discover later still delivers, but it cannot be backdated past that boundary. And the identifiers you send matter more than usual here: with no live browser, the event carries whatever hashed email, phone and stored click ID you kept from the original visit. The mechanics of that path are covered in tracking SaaS trial conversions to Meta Ads.

Does the same event_id work for TikTok, Pinterest, and Microsoft?

The shape is the same everywhere; the field name and the exact pairing rules differ. TikTok, Pinterest and Microsoft all deduplicate a browser event against a server event on a shared event ID plus a matching event name — the same contract Meta uses, which is why one internal ID per conversion is the right internal design.

What you should not do is share one literal string across destinations without scoping it. Include the destination’s config or tag ID in the hash input so a retry against one platform can never be mistaken for a send against another, and so your own dedup table stays keyed per destination. If you are running Microsoft alongside Meta, the Bing Ads conversion tracking guide covers the UET-side pairing in more detail.

How does PartialLeads handle event_id deduplication?

PartialLeads builds every outbound event_id as a SHA-256 hash of pixel_id | event_name | record_id, and every dispatch table is keyed UNIQUE(config, event_id). That combination is what makes the server side idempotent end to end: a redelivered webhook, a retry after a transient failure, a manual resend from a lead, and the catch-up worker all compute the same string and the database physically refuses the second write.

It matters because there are five separate dispatch sites in the product — the hot path, the catch-up worker, real-time conversion, the ecommerce fan-out, and inbound purchase processing — so that no signal path silently drops a conversion. Five paths to one destination is exactly the architecture that double-counts if the IDs are not deterministic.

The CAPI activity log is where you see it working. Each row carries the event name, the event_id, the destination platform, and a status — sent, queued, deduped, or failed with the platform’s own error text. A conversion that arrived twice shows as one sent row and one deduped row against the same ID, which is the difference between “we dropped it” and “we recognised it.”

PartialLeads CAPI activity log showing a sent Purchase row and its deduped twin sharing one event_id, with platform chips, delivery health and the UNIQUE(config, event_id) dedup key

Three honest constraints.

PartialLeads controls its own sends, not your Pixel’s. If your browser Pixel fires its own Purchase with a random eventID — a hand-rolled tag, a GTM template, a theme app — those two events carry different IDs and Meta has no reason to pair them. Either feed the same deterministic ID into the Pixel call, or lean on Meta’s fbp/external_id fallback and accept that it is weaker.

The base tag still matters. Deduplication and match quality are different problems with a shared dependency: the _fbp cookie. PartialLeads reconstructs _fbc from a fbclid in the URL, but it does not synthesise _fbp out of nothing. Keep your client-side base tag installed.

Deduplication is not a repair for double-counting inside your own store. If your platform genuinely creates two orders for one sale, two distinct record IDs exist and both are legitimately unique conversions. That is an order-data problem upstream of anything a tracking tool can see.

What breaks The mechanism Where you see it in the dashboard
Random or per-pageview event IDs Deterministic SHA-256 of pixel_id, event_name and record_id CAPI activity log, event_id column
Redelivered webhooks firing a second conversion Dedup tables keyed UNIQUE(config, event_id) CAPI activity log, deduped rows
Multiple dispatch paths racing for one sale All five dispatch sites derive the same ID from the same record CAPI activity log, one sent row per conversion
Retries after a transient platform failure Retry taxonomy: transient retried, permanent not retried, auth flagged for reconnect CAPI activity log status and error text
The same order sent to several ad platforms Destination config included in the ID input, dedup keyed per config Per-platform activity logs
Late conversions with no browser event Record-derived ID plus stored identifiers, event_time inside Meta’s 7-day window CAPI activity log, per-lead API column

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Sources

https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event https://developers.facebook.com/docs/marketing-api/conversions-api/using-the-api https://developers.facebook.com/docs/meta-pixel/reference


Frequently asked questions

QCan I just use the order ID as the event_id?
Yes, and for Purchase events it is usually the best choice — it is already unique, already on both sides, and already stable across retries. The only reasons to hash it are to avoid exposing your internal order numbering to a third party and to keep one consistent ID format across every event type. Meta treats the value as an opaque string either way.
QDoes the event_id need to be unique across event types?
Yes. Meta pairs events on event_name plus event_id, so in theory the same ID under two different names is safe — but it is a bad habit. A Lead and a Purchase that share an ID will collide the moment something normalises event names or an internal dedup table keys on the ID alone. Include the event name in whatever you hash.
QWhat happens if the Pixel event and the server event have different event_ids?
Meta records two conversions. Your reported purchase count and revenue inflate, and the optimisation signal degrades because the algorithm learns from events that did not happen. Meta may still pair them through the fallback method if both carry the same event name and a matching fbp or external_id, but that depends on a browser cookie being present and is not something to rely on.
QHow long does Meta keep an event around for deduplication?
Meta's documented window for receiving the redundant pair is roughly 48 hours. A server event that arrives three days after its browser twin will not deduplicate against it. This is separate from the seven-day event_time limit, which governs how far in the past an event may be timestamped rather than how far apart the pair may arrive.
QShould I send an event_id on events that only fire server-side?
Yes. Even with no Pixel event to pair against, the ID makes your own retries idempotent — webhook redeliveries, catch-up workers, manual resends and backfills all recompute the same value instead of creating new conversions. Deduplication against the Pixel is one job the event_id does; protecting you from yourself is the other.
QDoes deduplication fix double-counting between my ad platform and my CRM?
No. Deduplication only operates inside one platform's dataset, between two events it can see. If Meta and Google both claim the same sale, or if your CRM counts a lead the ad platform also counted, that is an attribution-model disagreement, not a duplicate event. It needs a single source of truth for revenue, not a shared ID.
QCan I change the event_id format later?
You can, but change it on both sides in the same deploy. During any window where the Pixel uses the old format and the server uses the new one, nothing deduplicates and every conversion counts twice. Old events already in the dataset are unaffected — they were deduplicated at the time they arrived.
QIs a UUID ever the right choice for an event_id?
Only if it is stored with the conversion record the moment it is generated, so every later send reads the same value rather than generating a new one. A UUID written to the order row is fine. A UUID generated at send time is the single most common cause of duplicate conversions.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.