pixellint

CAPI · engineers · marketers · PMs

Two pipes, one conversion

The pixel still sees cookies and the page URL. The server still sees the order. Together they cover users who blocked the tag and users whose backend sale never hit thank-you. Apart they lie in opposite directions.

Why both

Pixel-only: ad blockers, ITP, consent that never unblocks the tag, and hosted checkouts on another origin punch holes. The thank-you page is a suggestion. CAPI-only: you must collect click ids and cookies yourself or match rates collapse, because the POST comes from your VPC, not from the shopper's browser.

Both: the platform keeps the first event it trusts and drops the duplicate if the dedup key matches. Meta, TikTok, and Pinterest all document this hybrid. Dropping one pipe because the other 'works in Test Events' is how you undercount blockers or double-count everyone else.

What has to be identical

Event name, event_id, and the same Pixel ID (or the TikTok pixel_code / Pinterest tag the browser used). Meta's pixel eventID (camelCase, fourth argument to fbq) must match CAPI event_id. The event must match event_name. Dedup is not 'same value and same minute'. A Purchase pixel paired with a purchase CAPI event is two events.

Value and currency should match or ROAS reporting fights itself. User data can be a union: the pixel has _fbp and _fbc, the server has hashed email. That union is the point of the hybrid. Omitting fbp on CAPI because 'the pixel already sent it' leaves the blocked-pixel users unmatched.

The 48 hour window is not a retry budget

Meta's dedup window is 48 hours after the first event with that id, on the same Pixel ID. A delayed CAPI hit with a new UUID is a second Purchase. A delayed CAPI hit with the original id still collapses if it arrives inside the window. After the window, the same id will not save you.

Pinterest documents event_id as required on CAPI (vendor.pinterest-conversions-api.body.event_id.missing). TikTok documents event_id as required when the same conversion is sent from both pipes. Meta marks event_id recommended; vendor.meta-conversions-api.body.event_id.missing is the empty case. Recommended is not optional if you run both pipes.

What goes wrong

Different event names (Purchase vs purchase). Random UUIDs generated twice, once in GTM and once in the queue publisher. Server retry that mints a new id. Pixel on all pages, CAPI only on paid orders. Pixel Purchase on thank-you, CAPI Purchase on authorize, then again on capture.

You will not notice in Test Events if you only watch one pipe. Pixel Helper showing a hit does not prove CAPI arrived with the same key. Test Events showing a server event does not prove the browser used eventID.

Mint once, pass it across

On the server when the order id exists, then pass it to the page for fbq or ttq. Or mint on the page and POST it to your backend before CAPI. The order id itself is a fine event_id if it is unique per event type (a refund of that order needs a different id or a refund event, not a second Purchase).

Do not generate in both places and hope. Do not use a timestamp. Two checkouts in the same second will collide, and a retry will look like a new order if you also bump the clock.

fbq eventID and CAPI event_id

The pixel parameter is camelCase. The server field is snake_case. Same string. If only one side sets it, there is nothing to dedup against.

// Browser: Meta pixel. eventID is camelCase.
fbq('track', 'Purchase', { value: 84.50, currency: 'USD' }, { eventID: 'order-1842' });

// Server: same Pixel ID, same event_name, same id, seconds not milliseconds.
{
  "data": [{
    "event_name": "Purchase",
    "event_time": 1770000000,
    "event_id": "order-1842",
    "action_source": "website",
    "event_source_url": "https://shop.example/thanks?order=1842",
    "user_data": {
      "em": ["64-char-sha256-hex"],
      "fbp": "fb.1.1770000000.1234567890",
      "fbc": "fb.1.1770000000.AbCdEf",
      "client_ip_address": "203.0.113.10",
      "client_user_agent": "Mozilla/5.0 ..."
    },
    "custom_data": { "value": 84.50, "currency": "USD" }
  }]
}

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.