pixellint

Vendor · engineers · marketers · PMs

fbq plus CAPI, same event_id

This is the setup Meta documents. The rulepacks catch the usual payload bugs. The playbook is the operational part: one id, two pipes, seconds not milliseconds.

Browser pipe

fbq loads the pixel. Requests go to facebook.com/tr. id is the numeric Pixel ID. ev is required; unrecognized values warn because custom events are legal, which is why Purchse still 200s. noscript, when present, is 0 or 1. The image fallback is a different request, not a second copy of the JS event. A raw email in the query is vendor.meta.pii.unhashed_email. Two base codes (theme plus GTM) are two PageViews. Pixel Helper will show both.

Purchase on the wire still needs value and currency. Meta documents the requirement; the browser spelling cd[value] and cd[currency] is ecosystem evidence. Currency is ISO 4217 (USD), not a dollar sign. Limited Data Use is dpo=LDU with dpoco (1 for the United States, 0 to geolocate) and dpost (numeric state, or 0). Country without state lets Meta geolocate: vendor.meta.ldu.country_without_state. The pack page /packs/meta/ is the field table. This playbook is how that hit pairs with CAPI.

fbq("track", "Purchase", { value: 19.99, currency: "USD" }, { eventID: "T12345" });
pixellint validate url @tr.txt --rulepack vendor/meta

Server pipe

POST the Graph API events edge with access_token. Per event: event_name, event_time exactly 10 digits (Unix seconds), action_source from the documented set, user_data required. website requires event_source_url of the page, not api.yoursite.com/capi. event_id is expected so the pixel can dedup. Pixellint: vendor.meta-conversions-api.body.event_time.invalid on 13 digits. test_event_code present warns because it diverts to Test Events. Production tokens plus a test code is still your real pixel id.

em, ph, fn, ln, ge, db, ct, st, zp, country, and external_id are SHA-256 hex. Upper-case hex is allowed: Meta documents lower-casing the input, not the digest. client_ip_address and client_user_agent must not be digests (hashed_plaintext_field). fbp and fbc match fb.N.timestamp.value, plaintext. Purchase needs custom_data.value and custom_data.currency. HTTP 200 is not a schema review. Pack page: /packs/meta-conversions-api/.

{
  "data": [{
    "event_name": "Purchase",
    "event_time": 1770000000,
    "event_id": "T12345",
    "action_source": "website",
    "event_source_url": "https://shop.example/thank-you",
    "user_data": {
      "em": ["64CHARHEXDIGEST"],
      "ph": ["64CHARHEXDIGEST"],
      "fbp": "fb.1.1770000000.123",
      "fbc": "fb.1.1770000000.fbclid",
      "client_ip_address": "203.0.113.10",
      "client_user_agent": "Mozilla/5.0"
    },
    "custom_data": { "value": 19.99, "currency": "USD" }
  }]
}

eventID versus event_id

The browser JS API names the fourth-argument key eventID (camelCase). CAPI JSON names it event_id. Same string. fbq's event must equal CAPI event_name, including PascalCase Purchase. Meta discards later copies of the same pair on the same Pixel ID for 48 hours after the first. Dedup is not matching. Matching uses em, fbp, fbc, IP, UA. If the browser mints a UUID and the server mints another, you reported two purchases.

Mint the id once: order id, or a UUID created on the page and POSTed to your backend. Replays must reuse the id and the original event_time. Do not refresh the clock on retry. A 500 is not permission to mint a new id. Delayed offline uploads must keep the original id even if you send late. Empty Test Events means you are not live, even if GTM says the tag fired. Watch Pixel Helper and Test Events together.

Clock and hashing

event_time is Unix seconds. Date.now() is milliseconds. Math.floor(Date.now() / 1000). A 13-digit value is tens of thousands of years in the future, outside the window (events may be up to 7 days old). Amplitude and LinkedIn want the 13-digit millisecond value you must not send here. GA4 wants 16-digit microseconds. TikTok wants ISO 8601. Copying this client to those vendors is a clock bug generator.

Hash the allowlist after Meta's normalization (trim, lowercase email, E.164-style phone digits). Leave IP, UA, fbp, fbc, click ids, and event_source_url in the clear. Send the client IP, not the VPC NAT. A generic hash-all-strings helper is how Event Match Quality dies. Pixellint flags the inverse bugs: raw email, hashed IP, 13-digit time.

pixellint validate json @capi.json --rulepack vendor/meta-conversions-api

Independence and both pipes

Pixel-only: ad blockers and ITP punch holes. CAPI-only: you must collect click ids and cookies yourself or match rates collapse. Both: Meta keeps the first event it trusts and drops the duplicate when event_id and event_name match. You can launch ads on a pixel-only stack. You cannot call it the recommended CAPI setup until both pipes exist.

Pixellint is not affiliated with Meta. The packs cite Meta's pixel and Conversions API docs. If the docs move, the pack should move, not your memory of 2021. Pack pages under /packs/meta/ and /packs/meta-conversions-api/ are the contracts. This page is the playbook around them: two pipes, one id, seconds, hashed em/ph, plaintext IP and UA. pixellint validate url on a /tr request and pixellint validate json on the CAPI body.

Check the artifact

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