CAPI · engineers · PMs
One event_id, two transports
Dedup keys are not optional glue. If the browser mints a UUID and the server mints another, you reported two purchases. If neither sends a key, the platform may still try to guess from time and value, and guess wrong.
Where to mint it
On the server when the order id exists, then pass it to the page for the pixel. Or mint on the page and POST it to your backend before CAPI. Persist it on the order row. The id has to survive a refresh of thank-you, a second webhook, and a 500 from the Graph API.
The order id itself is a fine event_id if it is unique per event type. Do not reuse the same id for AddToCart and Purchase. Meta dedups on the pair (event_id, event_name) plus Pixel ID, not on the id alone. Two different names with one id are two events, which is what you want for a funnel.
Pixel spelling versus CAPI spelling
Meta's browser API takes eventID (camelCase). CAPI takes event_id. They must be the same string. TikTok's pixel and Events API both use event_id. Pinterest requires event_id on CAPI even if you forgot the tag; vendor.pinterest-conversions-api.body.event_id.missing and .empty are errors, not warnings.
A helper that sets event_id on CAPI and event_id (snake) on fbq does nothing on the pixel. Read the vendor's browser SDK, not your JSON serializer.
Window
Meta keeps a 48 hour dedup window after the first event with that id, on the same Pixel ID. Vendors do not keep your key for weeks. A delayed offline upload with a new id will double-count against a pixel that already fired. Use the original id even if you send late, and still respect the maximum event age (Meta event_time can be up to 7 days old).
Late plus new id is the worst pair: the pixel already counted, the server counts again, and you cannot collapse them. Late plus original id may still collapse inside the window, or it may miss the window and still be the honest single key for your own warehouse.
Retries are the same event
A 500 from Meta is not permission to mint a new id. Replay the same body. Idempotency here is your event_id, not their HTTP layer. At-least-once queues will redeliver. If the publisher calls uuid() in the send path, every redelivery is a new conversion.
Store the vendor acknowledgement against the order. A nightly reconciler that resends last week's paid orders with fresh UUIDs is an incident, not a backfill. Resend with the original ids only if the vendor never acknowledged.
What the platform guesses without a key
Some vendors try to match pixel and server events on time, value, and approximate user. That guess fails when the pixel fires on thank-you and CAPI fires when the payment capture webhook arrives five minutes later, or when tax makes the two values differ, or when one side is missing currency.
Do not design for the guess. Pixellint flags a missing Meta event_id as vendor.meta-conversions-api.body.event_id.missing because Meta documented the field for this job. It cannot prove the pixel used the same string. That proof is in Pixel Helper plus Test Events, both pipes, one id.
Generate once
Wrong is uuid() in the pixel tag and uuid() in the worker. Right is one stored key on both transports.
// Wrong: two generators, two Purchases.
fbq('track', 'Purchase', { value: 20, currency: 'USD' }, { eventID: crypto.randomUUID() });
// later, in the worker
body.event_id = crypto.randomUUID();
// Right: persist on the order, reuse on retry.
fbq('track', 'Purchase', { value: 20, currency: 'USD' }, { eventID: order.event_id });
POST { "event_name": "Purchase", "event_id": "order-1842", "event_time": 1770000000, ... }
pixellint validate json @capi.json --rulepack vendor/meta-conversions-api
# vendor.meta-conversions-api.body.event_id.missing when the server field is absent
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.