Hashing Identifiers
Surfside links your records to its identity graph on SHA-256 hashes of email and phone, and on raw mobile device IDs. We never need plaintext contact details. A hash only matches when both sides prepared the input the same way, so this page fixes the preparation once, gives you code that does it, and gives you test vectors to prove your output before the first file ships.
Accepted identifiers
| Identifier | Prepare | Hash | Column |
|---|---|---|---|
| Email address | Remove all whitespace, then uppercase the whole address. Change nothing else. | SHA-256, 64 lowercase hex characters | email_sha256 |
| Phone number | Drop any extension, keep digits only, keep the last ten. US and Canada. | SHA-256, 64 lowercase hex characters | phone_sha256 |
| Mobile device ID | IDFA or GAID exactly as the operating system issued it. | None. Send it raw. A hashed device ID cannot be matched. | device_id |
| Client customer ID | Your own stable identifier for the person. It is the audience key and must not change between files. | None | ccid |
SHA-256 is the only accepted hashing algorithm. MD5 and SHA-1 hashes cannot be matched and will produce zero matches with no error.
Email addresses
- Remove all whitespace. Spaces, tabs and line breaks, at the ends and inside the address.
- Uppercase the whole address.
jane@example.combecomesJANE@EXAMPLE.COM. We match on the uppercase form. - Change nothing else. Keep plus tags, dots and subdomains exactly as the customer typed them.
- Hash with SHA-256. Write the digest as 64 lowercase hex characters with no
0xprefix. - No address, no hash. Leave the field empty rather than hashing an empty string.
| Input | " Jane.Doe+promo@Example.com " |
| Prepared | JANE.DOE+PROMO@EXAMPLE.COM |
| SHA-256 | 3a0bf6bc8d0a10c0bd321cc5694fc0583beb463e3b337e0a804e1ce91e576a3e |
Phone numbers
- Drop any extension first. Cut
x204,ext. 7and everything after it before you strip characters, or the extension digits end up inside the number. - Strip every non-digit. Spaces, dashes, dots, parentheses and the plus sign all go.
- Keep the last ten digits. This drops a leading
1country code. Fewer than ten digits is not a usable number: leave the field empty. - Hash with SHA-256. Write the digest as 64 lowercase hex characters with no
0xprefix.
| Input | "+1 (415) 555-0132" |
| Prepared | 4155550132 |
| SHA-256 | 983c25606b8b0ac842c0bb1b723f3f600de24cd8a792d6e29a7da88d732af0be |
EU data files must not contain phone numbers, hashed or otherwise. An international number is cut to its last ten digits and will not match.
Hashing examples
All three reproduce the test vectors above. Verify that before you build anything else.
Python
import hashlib, re
def sha256(s):
return hashlib.sha256(s.encode()).hexdigest()
def email_sha256(raw):
s = "".join(raw.split()).upper()
return sha256(s) if s else ""
def phone_sha256(raw):
number = re.split(r"\s*(?:x|ext)", raw, flags=re.I)[0]
digits = re.sub(r"\D", "", number)[-10:]
return sha256(digits) if len(digits) == 10 else ""
JavaScript
const { createHash } = require("crypto");
const sha256 = s =>
createHash("sha256").update(s).digest("hex");
const emailSha256 = raw => {
const s = raw.replace(/\s+/g, "").toUpperCase();
return s ? sha256(s) : "";
};
const phoneSha256 = raw => {
const number = raw.split(/\s*(?:x|ext)/i)[0];
const digits = number.replace(/\D/g, "").slice(-10);
return digits.length === 10 ? sha256(digits) : "";
};
Snowflake SQL
with prepared as (
select
ccid,
nullif(upper(regexp_replace(email, '\\s', '')), '') as email_prepared,
right(regexp_replace(regexp_replace(phone, '\\s*(x|ext).*$', '', 1, 0, 'i'), '[^0-9]', ''), 10) as phone_prepared
from customers
)
select
ccid,
sha2(email_prepared, 256) as email_sha256,
sha2(iff(len(phone_prepared) = 10, phone_prepared, null), 256) as phone_sha256
from prepared
Before you send
- Every hash is exactly 64 lowercase hex characters. No
0xprefix, no uppercase hex, no base64. - Your output for the two test vectors matches this page character for character.
- Send both
email_sha256andphone_sha256on every record that has both. We match on either and email wins when they disagree. - An unknown value is an empty cell. Never a placeholder, never the hash of an empty string.
- No plaintext email or phone anywhere in the file, including in free-text columns.
- Column headers and the
ccidstay identical across every file you send. - UTF-8, one record per line, comma delimited, named
Partner-YYYYMMDD-dataset.csv, gzipped if large. See Batch Upload File Format for the full file contract. - No salting, no peppering, no hashing a hash. Our join is an exact string match, so any of these produce zero matches and no error.
Summary and best practices
One rule for every file: prepare the value exactly as this page describes, hash it once with SHA-256, send lowercase hex or nothing. Everything else is about keeping that rule stable over time.
- Hash inside your export job. Put the preparation code in the pipeline that writes the file, not in a spreadsheet step. Every file then comes from the same code path.
- Keep plaintext on your side. Only the hashes and your customer ID leave your systems. Keep your own mapping from ID to person so you can act on what comes back.
- Send every identifier you hold. A record with both an email and a phone hash matches more often than either alone. Never drop a record because one is missing.
- Re-run the test vectors on every change. A new library version or a refactor can shift output by one character, and one character is a zero match rate.