Blog · Meta pixel
Meta pixel vs Conversions API lines up a browser hit with a server event on the same dataset.
Meta pixel vs Conversions API, and Facebook pixel vs CAPI, read like a vendor shortlist. Someone is choosing. Meta's own implementation guide does not offer that choice as the recommended setup. It tells you to send the event from the browser and again from the server, with one shared id, and to let Meta keep one of them.
The shortlist is understandable. The pixel is a script and a cookie. CAPI is a POST your server controls. Ad blockers, ITP, and a checkout on another host punch holes in the first. A server that never saw fbclid punches a hole in the second. Teams pick the hole they are staring at and call the other pipe legacy.
The production miss is not the existence of two products. It is a migration that turns the pixel off, or a dual setup that generates the browser id and the server id in two places. Both still return success. Events Manager will show events. The purchase count will not match the orders table, in one direction or the other.
Meta calls the pair a redundant setup
The end-to-end guide's recommended row is a redundant setup: send events through the Pixel and through the Conversions API. You have to generate one persistent id and send the same event name and the same id on both. The guide also describes a server-only implementation, and it tells you to get the redundant setup or a split setup working before you switch to server-only.
Server-only is a real configuration. It is the configuration you earn after the join works, not the configuration you get by deleting the pixel tag because CAPI returned 200 in a test. The pixel still carries the page URL, the click, and the browser identifier that the VPC does not have unless you forwarded them.
A split, where some events stay on the pixel and others move to the server, is also documented. A Purchase that fires on both without a shared id is not a split. It is two purchases. A Purchase that fires only on the server, with no fbc, is a thinner match than the redundant event Meta asked for.
eventID and event_id are the same string with two spellings
Meta's deduplication page states the join in two sentences. The Pixel's eventID has to match the Conversions API event_id. The Pixel's event has to match the Conversions API event_name. The browser argument is camelCase, passed as the fourth argument to fbq. The server field is snake_case inside data[]. Same characters. Different key.
Purchase and purchase are not the same event name. A pixel that tracks Purchase and a worker that posts purchase will not deduplicate, because the names do not match. A pixel that tracks Purchase and a worker that posts CompletePayment, copied from a TikTok helper, will not deduplicate either. The order id can be identical and still fail the name check.
Meta marks event_id as recommended on the server event, which teams read as optional. Recommended is the field the redundant setup depends on. If only the pixel sets eventID, there is nothing on the server to match. If only the server sets event_id, there is nothing on the browser to match. Test Events can show a healthy server row while the browser row remains a second conversion.
One id, two calls
Illustrative pair from Meta's pixel reference and server event parameters. eventID on fbq and event_id on the server are the same string. event and event_name are both Purchase.
fbq('track', 'Purchase', {
value: 84.50,
currency: 'USD'
}, {
eventID: 'order-1842'
});
{
"data": [{
"event_name": "Purchase",
"event_time": 1759010400,
"event_id": "order-1842",
"action_source": "website",
"event_source_url": "https://shop.example/thanks?order=1842",
"user_data": {
"em": ["<sha256 hex>"],
"fbp": "fb.1.1759010400123.1234567890",
"fbc": "fb.1.1759010400123.AbCdEf",
"client_ip_address": "203.0.113.10",
"client_user_agent": "Mozilla/5.0"
},
"custom_data": { "value": 84.50, "currency": "USD" }
}]
}
The browser call does not send event_time. The server call does, and it has to be Unix seconds, at most 10 digits. A JavaScript Date.now() value has 13 digits. Read as seconds, that integer sits tens of thousands of years past the epoch. Meta holds event_time to the past 7 days, and to about ten minutes into the future. A value older than 7 days fails the batch, and no events in that request process. A 13-digit value misses both edges. The pixel has no event_time, so the helper can stay green while the server request does not. That is a CAPI failure beside a healthy pixel, which is the split a pixel-vs-CAPI argument hides.
Value and currency should match because the first event to arrive is the one Meta keeps. The dedup page says later copies are discarded. A pixel that sends 84.50 and a server that sends 8450, because the store keeps cents, do not average. If the pixel is first, 84.50 remains and 8450 is dropped. If the webhook is first, 8450 remains and the thank-you pixel is the discarded copy. Dedup does not reconcile the amount. Arrival order picks it. Google's transactionId match does the other thing: the later upload can replace the value. These two pipes are not one policy.
The Network row does not use the server's names
On the wire, the pixel request to facebook.com/tr uses id for the Pixel ID and ev for the event name. Purchase value and currency show up in captures as cd[value] and cd[currency]. eventID is the fourth argument to fbq. It is not a query key named event_id. The server body uses event_name, event_id, event_time, action_source, event_source_url, user_data, and custom_data.
Someone comparing the two logs will search the pixel request for event_name and decide the pixel never sent the purchase. It sent ev. They will search the server body for ev and decide CAPI dropped the name. It sent event_name. The redundant setup is one event only after those keys are translated. Equality is the string Purchase on both sides, not the key it sits under.
A noscript image is a second request, with noscript set to 1, not a replay of the fbq call. If the image and the script both fire and only one of them carries the id, the page did not dedupe them by including both. Two base codes, one in the theme and one in Tag Manager, are two PageView hits before any purchase worker runs. That double count is a pixel problem. Adding a server event does not collapse it.
The 48 hour window is the join, not a retry budget
Meta says that if the server key (event_id and event_name) and the browser key (eventID and event) arrive for the same Pixel ID within 48 hours, later copies are discarded. Events are deduplicated only when the second copy is received inside that window of the first. A warehouse job that posts yesterday's purchases on Monday with a new UUID misses the window and adds a second Purchase. The same job with the original id still collapses, if it lands inside 48 hours.
Retries have to reuse the id. A queue that treats a timeout as a new event will mint a new event_id and a new event_time. Meta will count both if the names match a real purchase shape. Idempotency is the id you already sent, not a fresh one per attempt.
The order id is a sound event_id when it is unique per event type. The refund of order 1842 needs a different id, or a refund event name, not a second Purchase with the same id. Reusing the purchase id on the refund makes the refund look like the duplicate Meta was told to drop.
The server cannot see fbp and fbc unless you pass them
The pixel writes _fbp and, when fbclid was on the landing URL, _fbc. CAPI reads user_data.fbp and user_data.fbc. Those values are not in the order row unless the landing hit stored them. A server-only Purchase with a hashed email and no click cookie is a legal event and a weaker match than the redundant event.
fbc is not the raw fbclid. The cookie shape is fb.1, a timestamp, and the click id. Pasting fbclid into user_data.fbc sends a string Meta did not format. Omitting fbc because the pixel already fired it leaves every blocked-pixel user without the click key. The redundant setup is a union: the browser has the cookies, the server has the confirmed order and the email.
Hosted checkout on another domain drops the first-party cookies. The landing host has to persist fbclid, and the order service has to receive it, before the CAPI worker runs. A pixel on the shop domain and a CAPI worker that only sees the payment provider's webhook will not share fbc. They can still share event_id if you passed the order id into the page. They cannot share a cookie the webhook never received.
For optimal ad performance, we recommend that advertisers implement the Conversions API alongside their Meta Pixel.
Meta: deduplicate pixel and Conversions API events
One order can still be three different ids
Landing is on www, with fbclid in the query. The pixel writes _fbp and _fbc there. Checkout is on pay.example, a different host. The thank-you pixel mints a fresh eventID in the container because the order id was never passed into that page. The payment webhook posts CAPI with event_id set to the order id, a hashed email, and no fbp or fbc, because the webhook never saw the cookies. If the worker copies the server's own address into client_ip_address, the IP is the NAT, not the shopper.
Meta discards a later event when event_id plus event_name match the browser's eventID plus event, on the same Pixel ID, within 48 hours of the first. In this checkout those strings differ, so nothing is discarded. You have a pixel Purchase on the pay host with no click cookie, and a server Purchase with the order id and no click cookie. The _fbc that mattered was written on the host that did not check out.
The repair is earlier than the POST. Persist fbclid and the browser id on the first host. Pass the order id into the thank-you page and use it as eventID. Pass that same order id, the stored fbp, and the stored fbc to the worker. Send the shopper's IP and user agent in the clear. Upper-case hex on the email digest is allowed. Meta's rule is to lowercase the address before hashing, not the digest after. Hashing the IP because the email is hashed is a different bug, and it removes a match key the server was supposed to add.
A retry that refreshes the clock is a new moment
Replays have to send the original event_id and the original event_time. A timeout is not permission to mint a new id, and it is not permission to stamp now if the first attempt may have landed. A delayed upload keeps the id from the original event even when the job runs the next day, as long as it still falls inside the windows you are actually in: 48 hours for dedup against the pixel, and 7 days for how old the server event may be.
test_event_code routes the body to Test Events. The access token and the Pixel ID stay the production ones. A pixel that is live in the helper, beside a server body that still carries the test code, is why the comparison looks like CAPI is down. Strip the field before live traffic. Do not send an empty string and call it stripped. Watch the helper and Test Events on the same purchase. One green check is one pipe.
Dedup is not match quality. The id decides whether the second copy is free. em, fbp, fbc, the client IP, and the client user agent decide whether the event Meta kept can be tied to a person and a click. A perfect shared event_id with an empty user_data block dedupes and still does not match. A rich user_data block with two different ids matches nothing to nothing and counts twice.
The pixel often arrives before the money is final
Place Order paints the thank-you page, and the pixel fires Purchase with the total the browser computed. The payment webhook lands a second or two later with the captured amount, after tax, after a partial authorization, after a line the client had not repriced. If both calls carry event_id order-1842 and event name Purchase, Meta keeps whichever one it received first and discards the other inside 48 hours. On a fast page the first one is the pixel. The settled amount is the duplicate.
Holding the pixel until the webhook has confirmed the capture flips that order. Then the server amount is first, and the pixel is the copy that gets dropped, which is what you want for the money and a loss for any browser key the server forgot to forward. Firing the pixel on the button click, before the order id exists, is worse: the pixel mints its own id, the server uses the order id, and nothing is discarded. You report two purchases, and the early one may have a value the capture never charged.
A refund is the same trap with the sign flipped. The purchase id has already been used. A second call with that id and event name Purchase, even with a negative value, is the duplicate Meta was told to drop. The refund needs its own event name or its own id. Google's match, by contrast, can replace the original value when the transactionId matches. Sending the refund as a second Meta Purchase will not produce that replacement.
action_source is not what keeps the second event
Meta's documented dedup key is the event name and the id, on one Pixel ID. action_source is not in that pair. The server still has to send it. The enum is email, website, app, phone_call, chat, physical_store, system_generated, business_messaging, or other, and website is not the default if you omit the field. website also requires event_source_url, and that URL has to be the page, not https://api.shop.example/capi.
Because the key ignores the channel, a physical_store Purchase that reuses the website order's event_id can be discarded as the later copy. If the store event arrives first, the website pixel is the one that gets discarded. Two real conversions, one id, one survivor. The fix is a different id per event type and per channel, or an explicit decision that the store upload is the only Purchase and the pixel does not fire one.
The Pixel ID is also in two places, and a mismatch looks like a pipe that failed. The browser request carries it as the id query parameter on facebook.com/tr. The server carries it in the Graph path, the events edge for that pixel. A test pixel on the server and the live pixel on the page are two datasets. Dedup never sees both. Events Manager will show a healthy pixel and an empty server, which is the search result people describe as CAPI versus the pixel.
fbc is milliseconds in the middle, and event_time is seconds
A real _fbp or _fbc looks like fb, a subdomain index, a creation time, and a value. The index is typically 1. The creation time is Unix milliseconds, the cookie's mint time. The last segment is a random browser id for _fbp, or the fbclid for _fbc. Send that cookie value unchanged. The shape check is fb, one digit, digits, then the rest. It does not check that the middle number is milliseconds, so a seconds value still looks legal.
event_time on the same JSON object is the opposite unit: ten-digit seconds. The Date.now() you must divide by 1000 before it can be event_time is the number that belongs, undivided, in the middle of fbc when you are rebuilding the cookie. One helper that divides every timestamp will fix the server clock and write fb.1.{seconds}.{fbclid}. That string is not the cookie the pixel set, which used milliseconds. Meta will not line it up with the browser id it already stored.
You can rebuild fbc when the cookie never existed or Safari has already expired the JavaScript cookie: fb.1, the landing time in milliseconds that you actually stored, then the fbclid you persisted server-side. You cannot rebuild _fbp. That random is the browser the pixel minted. A new random per purchase is a new person every time. One constant random is a single fake browser for the whole shop. Omit fbp when you do not have the cookie. A linter that only checks the shape will accept the fake.
The phone hash has to be the same digit string on both pipes
Meta's user_data.ph is SHA-256 of digits with a country code, symbols and letters removed, no leading plus. A US number stored as (650) 555-1212 becomes 16505551212, then the hash. A 10-digit national number without the leading 1 is a different digest. A UK number stored as 07700... needs the trunk 0 dropped when 44 is added. Hashing the display string passes a hex-length check and matches nobody.
The pixel's advanced matching and the server hasher are two implementations of that rule. If they disagree, one pipe attaches the purchase to a person and the other does not, even when event_id matches and the duplicate count is gone. Test one number you control on both paths before you upload a CRM file of locally formatted phones. Extensions and x123 suffixes are not part of the subscriber number. Letters left in the input mean you hashed a label.
Do not run that phone normalizer on fbp, fbc, or the client IP. Those stay plaintext. A privacy pass that hashes every string in user_data deletes the click cookie and the browser id in the same stroke that it finally formats the phone.
What to do when the search says to pick one
Run both for web purchases until dedup is visible: the same Pixel ID, the same Purchase name, the same event id, inside 48 hours. Mint the id once, when the order exists, and pass it to fbq as eventID and to the server as event_id. Forward fbp, fbc, client IP, and user agent onto the server event when you have them lawfully. Hash email after trim and lowercase. Send event_time in seconds.
Move to server-only only after that join is boring. Meta's guide puts server-only after the redundant or split setup, not before. If a blocker rate is the reason you added CAPI, turning off the pixel removes the browser keys CAPI was supposed to sit beside.
Pixellint is not affiliated with Meta. vendor/meta checks the browser request. vendor/meta-conversions-api checks the JSON body. A clean pair of checks is not Events Manager. It is evidence the two artifacts use the names and the clock the redundant setup requires. The field list lives on the pixel-plus-CAPI page. This page is the shortlist: pixel vs CAPI is one dataset, and the id is the only thing that makes the second event free.
Checks when both pipes are already on
- Compare fbq's event argument with data[].event_name. Purchase must match Purchase, including case.
- Compare the fourth-argument eventID with data[].event_id. Generate that string once.
- Confirm event_time is ten digits. Divide a millisecond clock by 1000 and floor it.
- Confirm both calls use the same Pixel ID.
- Persist fbclid on the landing request and send user_data.fbc in the fb.1.timestamp.fbclid shape. Do not put the raw query value in fbc.
- Replay a failed POST with the original event_id. Do not mint a new one on timeout.
- Give a refund its own id or its own event name. Do not send a second Purchase for the same order id.
- Record which pipe arrived first on a real order. That pipe's value is the one Meta keeps when the ids match.
- Do not reuse one event_id for a website Purchase and a physical_store Purchase.
- Confirm the pixel query id and the Graph pixel id are the same number. event_source_url is the page, not the API host.
- Confirm the id generator runs once per order, not once per tag read. A UUID function inside a variable runs again for the server tag.
- AddToCart and Purchase may share the order id. A second event named Purchase may not.
- If the amounts differ by tax, do not expect a match on time and value to collapse them. Fix the id.
- If you rebuild fbc, the middle number is the landing time in milliseconds. event_time on that same payload stays in seconds.
- Omit fbp unless you copied the cookie the pixel set. Do not mint fb.1 plus a random.
- Hash phone as country code plus digits, on both the pixel matcher and the server. (650) 555-1212 and 16505551212 are not the same input.
Sources
Contract pages
The dated argument is above. These pages are the field lists.