pixellint

Identity · engineers · marketers

E.164, then SHA-256, not a local format

US storefronts store (650) 555-1212; ads graphs store a country-coded digit string. If those two go into SHA-256, you minted two people.

What to hash

Build the E.164 number: country code, then the national number, digits only. Drop spaces, dashes, parentheses, and a leading plus. US (650) 555-1212 becomes 16505551212, then SHA-256. That digit string is what Meta documents for user_data.ph: symbols and letters removed, country code kept. Pinterest CAPI says the same: digits-only with country code, then SHA-256.

A leading plus is correct E.164 on a Twilio webhook and wrong in the hasher if the vendor's example has no plus. Strip it. A leading zero from a local national format is also wrong once the country code is present. +44 7700 900123 is 447700900123, not 4407700900123 and not 07700900123.

Pixellint checks that ph is 64 hex. It does not parse E.164. A SHA-256 of the display string (650) 555-1212 will pass the hex check and match nobody. The format rule is not a substitute for the normalizer test.

Country code is not optional

A 10-digit US number without 1 hashes to a different digest than the 11-digit E.164 form. Default the country from the shop locale or from a form field you actually collect. Guessing US on a UK checkout is how match quality looks random by market.

TikTok's phone_number is the same idea: normalize, then SHA-256, send the hex as context.user.phone_number. Do not send the display string in a second field of the same event. Do not send the E.164 in custom_data.phone while also sending a digest in user_data.ph of a different normalization.

libphonenumber (or the vendor SDK) is the usual way to go from a national number plus a region to E.164 digits. A regex that strips non-digits and prepends 1 will mangle every non-US form. If you only sell in one country, still store the country code in the helper so the next market does not inherit the US assumption.

Extensions and junk

Drop extension, x123, and SMS opt-in suffixes before you hash. They are not part of the subscriber number the graph has. Letters in the input are a signal you are hashing a label, not a phone. Meta's own note is symbols and letters removed. If letters remain, you did not finish.

Test with one number you control, on both the pixel advanced matching path and CAPI. If only one side matches, the normalizers disagree. Fix that before you scale the CRM upload. Offline uploads of Salesforce phones in (xxx) xxx-xxxx will miss every CAPI event that hashed E.164, and the other way around.

Plus, zeros, and trunk prefixes

E.164 on the wire of a telephony API often starts with +. The ads hasher wants digits. Strip +. Do not strip the country code because you stripped the plus. A UK number stored as 07700... needs the leading 0 dropped when 44 is prepended. That is a national trunk prefix, not a country code. Treating 0 as country code is a classic CRM bug.

NANP 10-digit numbers need the leading 1. Brazilian and Mexican numbers need their country codes, not 1. A default of 1 for every row in a global shop is how LATAM match quality never recovers.

Fixture both pipes

Pick one number. Assert the digit string before hash. Assert the hex after hash. Send that hex as user_data.ph and as context.user.phone_number in the TikTok fixture. Confirm pixellint validate json does not report unhashed_email (it should not) and does not report ph.invalid. Then send the display string by mistake and confirm the hex changed. That is the regression you want CI to catch.

Pixellint is not affiliated with Meta or TikTok. Country-code rules come from those vendors' hashing docs, not from a pack-invented phone parser.

// WRONG: hash the display string
sha256hex('(650) 555-1212');

// WRONG: US 10-digit, no country code
sha256hex('6505551212');

// RIGHT: E.164 digits, then SHA-256
sha256hex('16505551212');

Check the artifact

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