Launch · engineers
Hash the fields the vendor named, nothing else
A generic 'hash all strings' helper is how match quality dies. A generic 'log the body' helper is how email ends up in Datadog. Two helpers, two incidents.
Allowlist, not a blanket hash
1. Hash em, ph, and the documented advanced matching fields (names, city, state, zip, country, external_id on Meta) with SHA-256 hex after the vendor's normalization. 2. Leave client_ip_address, client_user_agent, event_source_url, fbp, fbc, click ids in the clear. 3. TikTok: hash context.user.email and phone_number; leave context.ip and context.user_agent plaintext. 4. Pinterest and Snap follow the same split. 5. Do not hash IDFA or GAID unless the MMP field is documented as hashed.
Pixellint flags a 64-character hex string in an IP or UA field as hashed_plaintext_field on the ads packs that contract it. A 64-character hex string in em that was hashed from the wrong input is a silent miss. Test with the vendor's sample email and sample digest.
// allowlist: em, ph, fn, ln, ge, db, ct, st, zp, country, external_id
// plaintext: client_ip_address, client_user_agent, event_source_url, fbp, fbc, fbclid
// SHA-256 hex of trim+lowercase email. Not bcrypt. Not MD5. Not HMAC.
Tests that must fail the build
1. Vendor sample email to sample digest in CI. 2. A raw email fixture must fail the pack (unhashed_email). 3. A hashed IP fixture must fail the pack (hashed_plaintext_field). 4. A 13-digit Meta event_time must fail. 5. A 13-digit GA4 timestamp_micros must fail. That is the whole unit test suite a lot of teams skip.
Warnings do not fail pixellint validate. If you care about missing client_id or recommended event_id, fail the job on those codes yourself. The binary exiting 0 on warnings is documented behavior, not a pass on identity.
pixellint validate json @fixtures/capi-good.json --rulepack vendor/meta-conversions-api
pixellint validate json @fixtures/capi-raw-email.json --rulepack vendor/meta-conversions-api
pixellint validate json @fixtures/capi-hashed-ip.json --rulepack vendor/meta-conversions-api
Logs are a second copy of the payload
1. Redact email, phone, and tokens before the logger. 2. Set retention. 3. No Slack pastes of CAPI bodies. 4. Staging data in staging. 5. Retry tables must not store the raw request that still contains plaintext PII you already hashed on the way out.
Event name, time, event_id, vendor status, maybe a truncated IP. That is enough to debug 200 versus dropped. You do not need the email in CloudWatch to know hashing failed. You need a boolean from a linter in CI.
Query strings and HARs
1. Raw email must not ride on facebook.com/tr or any collector query. 2. Copy as cURL is a PII leak if you paste it. Redact em= and cookies. 3. A HAR is a fired artifact plus cookies plus whatever PII the page put in query strings. Treat it as a password dump. 4. Never commit a live HAR. 5. Prefer staging HARs after sanitizing.
See HAR files for pixel debugging. Support will ask for a HAR. That HAR includes Authorization headers for your own app if the user was logged in. Pixel debugging does not need those.
Product analytics is a different allowlist
PostHog, Mixpanel, and Amplitude often want email in the clear for people search. Do not run the ads hasher on those destinations. Do not put ads click ids in product properties unless you meant to. Two pipelines, two lists. Document both next to the event dictionary.
Pixellint is not affiliated with Meta. The CAPI pack cites Meta's customer information parameters. When the field list moves, the pack should move. Your blanket hasher will not.
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.