All posts

Hashing Emails for Google and Meta: SHA-256 Is the Easy Part

By Rajesh Kumar, Founder, Attribi ·

A lead typed jane.doe@gmail.com. Lowercased, trimmed and hashed, it becomes a hash starting
831f6494, which is accepted and matches nobody. With Google's Gmail rule applied first, the string
is janedoe@gmail.com and the hash starts d6117306, the only one Google can match. Both are correct
SHA-256 and neither returns an error.

Your upload job reports success. Every row has a 64-character hash where the email used to be. The match rate is poor, and nothing anywhere says why.

The hash function is almost never the reason. SHA-256 is one line in every language and it is hard to get wrong. The part that goes wrong is the line before it: what the string looked like at the moment it was hashed.

Here is how to hash email for Google Ads. Lowercase the address and remove all whitespace. If the domain is gmail.com or googlemail.com, also remove the dots before the @ and cut the plus sign and everything after it. Then SHA-256 the result and send it as hex. Meta wants the first two steps only, which means one stored hash cannot serve both.

What does it mean to hash an email for a conversion upload?

Hashing an email for a conversion upload means turning the address a lead gave you into a fixed 64-character SHA-256 digest, after first rewriting it into the exact form the ad platform expects, so the platform can compare it against digests of its own users' addresses without receiving the readable address. The rewriting step is called normalisation.

The two halves fail differently. A wrong hash function tends to get noticed. A wrong normalisation produces a perfectly formed SHA-256 digest of a string the platform never hashed on its side. It is accepted and it matches no one.

Why does a wrong hash never throw an error?

One Gmail address written three ways. As typed, Jane.Doe+Shopping@Gmail.com hashes to c9b10784.
Lowercased and trimmed, which is the right string for Meta, it hashes to 0207fd58. With Google's
Gmail rule applied, janedoe@gmail.com hashes to d6117306. Sent to Google Ads, only the third can
match, and none of the three returns an
error.

Because a hash is one-way. The platform cannot look at 0207fd58... and see a dot was left in. It can only look the digest up, and "not found" is also what a real person with no account on that platform looks like.

SHA-256 also has no notion of close. Change one character of the input and the whole output changes. jane.doe@gmail.com and janedoe@gmail.com are the same inbox and their digests share nothing:

  • jane.doe@gmail.com gives 831f6494ad6be4fcb3a724c3d5fef22d3ceffa3c62ef3a7984e45a0ea177f982
  • janedoe@gmail.com gives d6117306485ed0e50afab3ac871e98f81699151f30281527d63ff5f233656c69

So the failure shows up as a lower match rate, weeks later, mixed in with every other reason a lead might not match. That is how it survived in our own pipeline.

How to hash email for Google Ads

Google publishes the rule in two developer guides that agree with each other: the Data Manager API formatting guide and the Google Ads API guide to enhanced conversions for leads. Both were read on 2026-10-05. In order:

  1. Convert the address to lowercase.
  2. Remove whitespace. The Data Manager guide says leading, trailing and intermediate, so a space in the middle of an address goes too.
  3. If the domain is gmail.com or googlemail.com, remove every dot before the @.
  4. For the same two domains, remove the plus sign and every character after it, up to the @.
  5. Hash the result with SHA-256.
  6. Encode the digest as hex or Base64 and tell the API which.

Steps 3 and 4 apply to those two domains and no others. Google's own examples: cloudy.sanfrancisco+shopping@gmail.com becomes cloudysanfrancisco@gmail.com, while user.name+NYC@Example.com becomes user.name+nyc@example.com with its dot and its plus intact. Strip dots from a work address and you have broken a hash that was fine.

One thing to know before you copy a rule from a Help Center page. Google's Help article on setting up enhanced conversions with Tag Manager lists the dot rule for gmail.com and googlemail.com and, when we read it on 2026-10-05, said nothing about the plus sign. The two developer guides list both. If you are writing code, follow the developer guides.

Encoding. The Data Manager API takes hex or Base64, and its ingest request has an encoding field that must be set whenever user data is sent. Google notes that case does not matter for hex and does matter for Base64. That makes hex the safer choice.

Phone. E.164: a plus sign, the country code, then digits only. Google's example turns (800)555-0100 into +18005550100. The plus sign is part of the string that gets hashed.

Names and address. Given name and family name are lowercased, trimmed and hashed, without prefixes such as Mrs. or suffixes such as Jr. Region code and postal code are sent as plain text.

The request itself is a different article. The Data Manager API guide covers the call, and the Salesforce walkthrough has a full payload.

How is Meta's Conversions API hashing different?

Four rows of steps. Google email: lowercase, remove all whitespace, for gmail.com and
googlemail.com remove dots and the plus suffix, then SHA-256 as hex or Base64. Meta em: trim,
lowercase, no Gmail rule, then SHA-256. Google phone: E.164 with the plus sign, for example
+16505551212. Meta ph: digits only with country code and no plus sign, for example
16505551212.

Meta's rules live on one page, the customer information parameters reference, also read on 2026-10-05.

For em, the instruction is to trim leading and trailing spaces and convert to lowercase. That is all of it. The page documents no Gmail rule, and its example, John_Smith@gmail.com to john_smith@gmail.com, has no dot or plus to settle the question. So as documented: dots stay, plus suffixes stay.

For ph, Meta says to remove symbols, letters and any leading zeros, and to include the country code. Its example turns (650)555-1212 into 16505551212. No plus sign. That one character is the whole difference from Google's format, and it makes the two hashes unrelated.

Then the part that catches people who learned on Google first. Meta marks zp and country as hashing required. Country is the lowercase two-letter ISO code, so us, hashed. Google wants both in the clear.

Google vs Meta, field by field

FieldGoogle (Data Manager API, Google Ads API)Meta (Conversions API)
EmailLowercase, remove all whitespace. gmail.com and googlemail.com only: remove dots before the @, drop + and what follows. Hash.em: trim leading and trailing spaces, lowercase. No Gmail rule documented. Hash.
PhoneE.164 with the plus sign: +16505551212. Hash.ph: digits only, country code included, no symbols, no leading zeros: 16505551212. Hash.
First and last nameLowercase, trim, no prefixes or suffixes. Hash.fn, ln: lowercase, no punctuation, UTF-8. Hash.
Postal codeNot hashed.zp: lowercase, no spaces or dashes, first five digits in the US. Hash.
CountryRegion code, two-letter ISO, not hashed.country: lowercase two-letter ISO. Hash.
Click identifiergclid, gbraid, wbraid: never hashed.fbc, fbp: do not hash.
EncodingHex or Base64, declared in encoding.Lowercase hex in the examples on Meta's page.

One address, hashed for each platform

Take a lead who typed Jane.Doe+Shopping@Gmail.com. These digests were computed with shasum.

String that gets hashedSHA-256 (hex)
Meta emjane.doe+shopping@gmail.com0207fd58e6fd97e845076b479943d66769329b04ef902c5d2ab0a0d46237b883
Googlejanedoe@gmail.comd6117306485ed0e50afab3ac871e98f81699151f30281527d63ff5f233656c69

And the phone number (650) 555-1212, United States:

String that gets hashedSHA-256 (hex)
Google+165055512121e231c66011e7a2d867a9cfae267a6aff103cf4913640b6e71a99850fc0ffbc8
Meta ph16505551212e323ec626319ca94ee8bff2e4c87cf613be6ea19919ed1364124e16807ab3176

A minimal version of the two email functions in Node:

const { createHash } = require('node:crypto')
const sha256 = (s) => createHash('sha256').update(s).digest('hex')

function hashEmailForMeta(email) {
  return sha256(email.trim().toLowerCase())
}

function hashEmailForGoogle(email) {
  const e = email.replace(/\s+/g, '').toLowerCase()
  const at = e.lastIndexOf('@')
  const domain = e.slice(at + 1)
  if (domain !== 'gmail.com' && domain !== 'googlemail.com') return sha256(e)
  const local = e.slice(0, at).replace(/\./g, '').split('+')[0]
  return sha256(local + '@' + domain)
}

The domain test is an exact comparison on the text after the last @, so notgmail.com and mail.gmail.com are left alone. Real code needs a guard this sketch leaves out: +promo@gmail.com has nothing left before the @ after stripping, and you should refuse to hash it. Otherwise every such row gets the same digest.

What must not be hashed?

Anything the platform needs to read. Click IDs are the common casualty. A gclid, gbraid or wbraid goes to Google exactly as it arrived on the URL, and which of the three you have depends on the device and the surface. Meta's fbc and fbp are marked do not hash, as are lead_id, the IP address and the user agent.

And as above, Google's region code and postal code stay plain while Meta's zp and country do not. A generic "hash all the PII columns" step in a pipeline will get one platform wrong whichever way it is written.

How do you test a hash before uploading?

Test the normalisation, since that is where the bug will be. Four checks, cheapest first.

  1. Reproduce the vendor's own example. Meta publishes inputs with their expected digests. Feed John_Smith@gmail.com to your email function and you should get 62a14e44f765419d10fea99367361a727c12365e2520f32218d505ed9aa0f62f. Feed it (650)555-1212 as a phone and you should get e323ec62.... If either differs, stop there.
  2. Run awkward inputs through the Google function. Jane.Doe@Gmail.com and janedoe+x@gmail.com must both collapse to the janedoe@gmail.com digest above. jane.doe@acme.com must keep its dot.
  3. Check the string outside your code. printf '%s' 'janedoe@gmail.com' | shasum -a 256 in a terminal. Use printf, not echo. echo appends a newline and you will hash that too.
  4. Send one event in test mode. The Data Manager API accepts validateOnly: true, which validates the request without executing it. Meta has test_event_code, which makes the event show up in the Test Events tab of Events Manager. Read Meta's warning first: events sent with a test code are not dropped and do count for measurement, so remove the code afterwards.

Step 4 proves the request is well formed. Neither test mode tells you that a hash belongs to a real user. Only steps 1 to 3 catch a normalisation bug before it costs you a month of matches.

If the hashes are right and enhanced conversions still shows nothing, the cause is somewhere else. That checklist is in Enhanced Conversions for Leads Not Working, and what the feature is for is in the enhanced conversions for leads explainer.

What we got wrong in our own code

Attribi had this bug until the week this was written. The fix is commit 7dfd19b in our repository, and the account below comes from that change and its commit message.

All three of our Google upload paths hashed the lowercased, trimmed email. That is Meta's rule. It is also correct for Google on every domain except two. For a lead who typed jane.doe@gmail.com, we uploaded a digest Google does not compute, so the match on email could not succeed for that lead. When we counted, about one in six of the leads with an email in production were Gmail addresses with a dot or a plus alias.

Nothing failed. Click-ID matching was unaffected, so a lead that carried a click ID still matched on it. The leads affected were the ones relying on the email, and those are exactly the leads the second key exists for.

The obvious fix was to change the email hash. We could not. The same stored hash is sent to Meta, LinkedIn, TikTok and Snapchat, and it is the key we use internally to match a CRM deal back to a lead. Changing it would have fixed Google and broken the rest. So Google got its own stored variant in a new column, computed by its own function, and the shared one was left alone.

Leads stored before the fix have no Google variant. For those, the upload re-hashes the raw email to Google's rule at send time, so they are corrected on their next upload. The unit tests now pin the function to the two examples in Google's formatting guide.

How Attribi handles hashing, and where it stops

Attribi is the tool I build. Hashing happens on our servers, when the lead is received. Each lead gets separate stored variants, among them a standard email hash for Meta and the platforms that share its rule, a Google email hash, an E.164 phone hash and a digits-only phone hash. Each upload reads the variant its platform expects. Google uploads go out with encoding set to HEX. The offline conversions page describes the upload itself, and the Google Ads integration page lists what is sent.

Our two paths do differ, and I would rather say how. The Google path removes whitespace anywhere in the address. The Meta path trims the ends only. Both follow the vendor's wording, so an address with a space in the middle produces a cleaned hash for Google and an unmatched one for Meta.

The limits:

  • We do not guess a country code. A phone number captured as 050 123 4567 has no country in it. For Meta we hash the digits as they are, leading zero included, which is not what Meta's rule asks for. For Google, a number stored without a plus sign gets one prepended to its digits, which is only right if the digits already began with the country code. Collect phones in international format on your form. We cannot repair them afterwards.
  • We send email and phone only. No names, no postal code, no country, on either platform.
  • A correct hash is not a match. If the lead used an address the platform has never seen, nothing we do to the string changes that.

Conversion uploads are a paid feature, starting on the Starter plan. Every new workspace starts with 30 days on the Pro plan, no card required, which is long enough to upload a month of real leads and watch what matches. The wider picture of the upload routes is in the Google Ads offline conversion import guide.

Frequently asked questions

Do I remove the dots from every email address before hashing for Google Ads? No. Google's rule applies only to addresses at gmail.com and googlemail.com. For every other domain the dots and any plus suffix stay in, and removing them produces a hash that will not match.

Does Meta want Gmail dots and plus suffixes removed before hashing? Meta's customer information parameters reference does not say so. For em it documents two steps, trimming leading and trailing spaces and converting to lowercase. Following the page as written, dots and plus suffixes stay in the string you hash.

Can I store one hashed email and send it to both Google and Meta? Only for addresses outside gmail.com and googlemail.com that contain no internal whitespace. For a Gmail address with a dot or a plus alias, the two platforms expect different strings, so the hashes differ. Store one variant per rule, or keep the raw address long enough to hash it for each destination.

Should the SHA-256 hash be sent as hex or Base64? Google's Data Manager API accepts either and requires you to declare which in the encoding field. Meta's documentation shows lowercase hex in all of its examples. Hex is the safer default, because Google treats hex as case-insensitive while Base64 is case-sensitive.

Should I hash the country and postal code? It depends on the platform. Google wants the two-letter region code and the postal code sent unhashed. Meta marks both zp and country as hashing required, with the country as a lowercase two-letter code.

Why does my hashed phone number match on Meta and fail on Google? Most likely the plus sign. Google expects E.164, where the plus is part of the hashed string, as in +16505551212. Meta's example hashes digits only, 16505551212. The two digests have nothing in common, so a hash built for one cannot be reused for the other.

The takeaway

When hashed matching underperforms, do not start with the hash. Print the string that went into it, for one Gmail lead and one phone number, and hold it against the vendor's page. If that string is not character for character what Google or Meta would have built from the same person, no amount of correct SHA-256 after it will help. If you would rather not own that code, start with 30 days on the Pro plan.


Attribi tracks every marketing touchpoint and pushes revenue back to your ad platforms, so campaigns optimize on closed deals — not raw form fills. See how it works or start free.