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.

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.”

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