Skip to main content

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​

IdentifierPrepareHashColumn
Email addressRemove all whitespace, then uppercase the whole address. Change nothing else.SHA-256, 64 lowercase hex charactersemail_sha256
Phone numberDrop any extension, keep digits only, keep the last ten. US and Canada.SHA-256, 64 lowercase hex charactersphone_sha256
Mobile device IDIDFA or GAID exactly as the operating system issued it.None. Send it raw. A hashed device ID cannot be matched.device_id
Client customer IDYour own stable identifier for the person. It is the audience key and must not change between files.Noneccid
caution

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​

  1. Remove all whitespace. Spaces, tabs and line breaks, at the ends and inside the address.
  2. Uppercase the whole address. jane@example.com becomes JANE@EXAMPLE.COM. We match on the uppercase form.
  3. Change nothing else. Keep plus tags, dots and subdomains exactly as the customer typed them.
  4. Hash with SHA-256. Write the digest as 64 lowercase hex characters with no 0x prefix.
  5. No address, no hash. Leave the field empty rather than hashing an empty string.
Input" Jane.Doe+promo@Example.com "
PreparedJANE.DOE+PROMO@EXAMPLE.COM
SHA-2563a0bf6bc8d0a10c0bd321cc5694fc0583beb463e3b337e0a804e1ce91e576a3e

Phone numbers​

  1. Drop any extension first. Cut x204, ext. 7 and everything after it before you strip characters, or the extension digits end up inside the number.
  2. Strip every non-digit. Spaces, dashes, dots, parentheses and the plus sign all go.
  3. Keep the last ten digits. This drops a leading 1 country code. Fewer than ten digits is not a usable number: leave the field empty.
  4. Hash with SHA-256. Write the digest as 64 lowercase hex characters with no 0x prefix.
Input"+1 (415) 555-0132"
Prepared4155550132
SHA-256983c25606b8b0ac842c0bb1b723f3f600de24cd8a792d6e29a7da88d732af0be
note

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 0x prefix, no uppercase hex, no base64.
  • Your output for the two test vectors matches this page character for character.
  • Send both email_sha256 and phone_sha256 on 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 ccid stay 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.