How to Hash PII for Meta, Google and TikTok Match Rates
One wrong normalization step produces a hash that never matches, and no error is raised. A precise reference for normalizing and hashing customer identifiers before they reach the ad platforms.

Hashing customer identifiers for a conversions API integration looks like a solved problem. Lowercase it, trim it, run SHA-256, send the hex digest. Every guide says roughly that, and every guide is roughly right.
Then your match rate comes back at 31% and nobody can tell you why.
The reason this is harder than it looks is a property of hash functions that works against
you here. A cryptographic hash is designed so that a one-character difference in input
produces a completely unrelated output. That is exactly what you want for security and
exactly what makes debugging match rates miserable: Jane@Example.com and jane@example.com
produce digests with no visible relationship, and the platform has no way to tell you that
your input was almost right. It receives a valid 64-character hex string, finds no user
matching it, and records a miss. No warning. No error. Just a number that is lower than it
should be.
This is a reference for getting the normalization right, per platform, before the hash.
The rule that explains every other rule
Normalization is not a cleanup step. It is the agreement between you and the platform about what the canonical form of an identifier is.
The platform hashed its own copy of a user's email using its normalization rules. You hash your copy using yours. The two digests match only if both sides produced byte-identical input strings. Everything below is an elaboration of that single requirement.
Which means the correctness question is never "is my normalization sensible?" It is "is my normalization identical to theirs?" Those come apart in places where a reasonable engineer would make a reasonable choice that happens to differ.
Email normalization
The universally required steps, consistent across Meta, Google, and TikTok:
- Trim leading and trailing whitespace.
- Lowercase the entire address.
- SHA-256 the result, output as lowercase hexadecimal.
" Jane.Doe@Example.COM "
-> trim -> "Jane.Doe@Example.COM"
-> lowercase -> "jane.doe@example.com"
-> sha256 -> "a1b2c3..." (64 lowercase hex chars)
Note that lowercasing the whole address is what the platforms specify, even though the local part of an email address is technically case-sensitive per RFC 5321. Follow the platform rule, not the RFC — you are trying to match their digest, not to be correct in the abstract.
The Gmail dot and plus question
This is where guides start inventing things, so be careful.
Gmail treats jane.doe@gmail.com, janedoe@gmail.com, and jane.doe+shopping@gmail.com
as the same mailbox. It is therefore tempting to strip dots and plus-suffixes before
hashing, on the theory that you are canonicalizing to the "real" address and will match
more users.
Do not do this unless the platform's current documentation explicitly tells you to. If the platform does not perform that transformation on its side, you have just guaranteed a mismatch for every Gmail address containing a dot — which is a very large number of your customers. You would be optimizing for a semantic notion of identity that the matching system does not share.
The platforms' published normalization requirements center on trimming and lowercasing. Where their documentation is silent on dot and plus handling, silence means do not transform — send the address as the user gave it, trimmed and lowercased. Check Meta's Advanced Matching documentation and the equivalent Google and TikTok pages for your integration before deviating, and re-check when you revisit the integration, because these specifications do change.
Phone number normalization
Phone numbers cause more match-rate loss than email, because there are more ways to write the same number and most systems store them inconsistently.
The target format is E.164: a plus sign, country code, then the subscriber number, with
no spaces, hyphens, parentheses, or dots. Some platform documentation specifies including
the leading + and some omits it — this is exactly the kind of per-platform detail to read
from the current docs rather than assume.
"(555) 123-4567" -> ambiguous: no country code
"+1 (555) 123-4567" -> "+15551234567"
"555-123-4567" (US) -> "+15551234567"
"07700 900123" (UK) -> "+447700900123"
The hard problem is not formatting. It is the numbers stored without a country code.
A US-collected 5551234567 and a UK-collected 07700900123 look like the same kind of
data in your database and are not. If you default every country-code-less number to your
home market, you will silently corrupt every international customer's identifier. The
options, in order of preference:
- Capture the country code at collection time. An international phone input component that stores E.164 from the start eliminates the problem permanently.
- Infer from an authoritative per-record signal — the customer's billing country, shipping country, or store locale. Not from the request IP, which is unreliable.
- Omit the phone number for records where you cannot determine the country with confidence.
Option three is genuinely better than guessing. A wrong hash is not a partial match; it is a miss that also consumes the slot where a correct identifier could have gone.
Hash the normalized plaintext, once
Three failure modes worth naming explicitly, because each appears in production regularly.
Do not double-hash. If a value arrives at your dispatch layer already hashed — perhaps your storage layer hashes at rest — hashing it again produces the digest of a hex string, not the digest of an email address. Systems that hash defensively at multiple layers hit this constantly. Establish exactly one layer that owns hashing.
Do not hash partially normalized values. Normalization must fully complete before the hash. There is no partial credit.
Do not hash empty or placeholder values. This is the most common silent bug of the set.
When a customer record has no phone number, the correct action is to omit the parameter
entirely. Hashing an empty string produces a perfectly valid-looking 64-character digest —
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, the SHA-256 of the
empty string, which you will recognize on sight once you have chased this bug once. It
transmits successfully, matches nothing, and appears in your logs as a populated field.
The same applies to placeholders your data pipeline may have introduced: null, N/A,
unknown, none, or a sentinel like noreply@example.com. All of them hash to something
that looks like real data and is not. If your match rate is mysteriously low and your
coverage looks suspiciously complete, check for these first.
Meta: Advanced Matching and Event Match Quality
Meta accepts a set of hashed customer parameters — email (em), phone (ph), first name
(fn), last name (ln), city, state, zip, country, date of birth, and gender — documented
in
Meta's Advanced Matching reference.
Not every parameter requires hashing; some, such as the click ID, are sent in plaintext.
The documentation specifies which.
Meta scores your events with Event Match Quality, on a 1–10 scale, visible per event type in Events Manager. EMQ reflects how successfully Meta could match your events to people, and it is the fastest feedback loop available for a normalization change.
Two properties of EMQ that shape how you use it:
- Identifiers compound. Sending email plus phone plus name plus location matches more reliably than email alone, because a failure on any single identifier can be recovered by the others. Send everything you legitimately hold and have a lawful basis to send.
- Quality beats quantity. Five identifiers where two are malformed can score worse than two clean ones, since malformed values contribute noise. Adding fields is not automatically an improvement.
TikTok and Google
TikTok requires SHA-256 hashing of emails and phone numbers before the data reaches
TikTok's servers, and uses its own Event Match Quality score. Notably, TikTok's
Advanced Matching guidance
states that even without a click ID (ttclid), email matching alone can lift Event Match
Quality into the 7–8 range — useful, because it means identifier matching is a genuine
substitute for click-ID matching rather than merely a supplement to it.
Google Enhanced Conversions accepts hashed email, phone, and name and address fields, with the same SHA-256 requirement and its own normalization specification. Google's rules have historically had per-field details that differ from Meta's, particularly around address components. Read Google's current Enhanced Conversions documentation directly rather than assuming parity with Meta — the fields overlap, but the specifications are maintained separately and independently.
The practical consequence of three platforms with three specifications is that a single global "normalize this email" helper is not sufficient once you send to more than one destination. You need normalization that is aware of the destination it is preparing data for.
The consistency requirement nobody plans for
Here is the failure that survives every code review and only shows up in the metrics.
Most stacks send conversion data from more than one place. A browser pixel with advanced matching. A server-side conversions API integration. An offline conversion upload from your CRM. A batch backfill someone wrote once. Each was probably built at a different time, by a different person, and each normalizes identifiers in its own code.
Two things break when those implementations diverge, and one of them is not obvious:
- Match rate degrades on whichever path is wrong. Expected.
- Deduplication breaks — and this one surprises people. Platforms deduplicate a server-side event against its browser counterpart using an event ID, but identifier consistency matters to how confidently they can reconcile the two. Divergent hashes for the same person from two sources can present as two people, meaning your carefully built server-side integration is now double-counting against your pixel.
The rule that prevents all of it: exactly one normalization implementation, shared by every code path that sends events. Not one per service. Not a copied function. One, imported everywhere, with tests covering the edge cases — mixed case, surrounding whitespace, international phone numbers, missing fields, placeholder values.
This is why identifier normalization deserves to be a single source of truth in your codebase in the same way your pricing logic is. It is not a formatting utility. Two implementations that disagree by one character produce two populations of hashes that can never be reconciled, and nothing in any dashboard will tell you that is what happened.
Verifying a change
After any normalization change, confirm it moved the number rather than assuming:
- Meta Events Manager shows Event Match Quality per event type. Give it 24–48 hours to stabilize before drawing conclusions; the score is computed over a window.
- Google Ads surfaces Enhanced Conversions diagnostics, including match rates and warnings about malformed identifier formatting. Those format warnings are the most direct signal available anywhere — read them first.
- TikTok Events Manager reports its own match-quality score per event.
On thresholds: practitioner guidance from AnyTrack suggests a match rate below roughly 40% usually indicates a formatting or normalization defect rather than genuinely absent signal. That is a useful rule of thumb rather than an official platform threshold — no platform publishes a number below which you are definitively broken. Treat it as a prompt to investigate, not a target.
The most useful diagnostic is not a threshold at all, though. It is hashing a known test value by hand, comparing it to what your pipeline produced for the same input, and finding out which of the two is wrong. Nearly every match-rate problem resolves to a normalization step someone assumed was happening and was not.


