How to generate the exact same hash file HashComp produces — on your own machine, without uploading data to this website.
These instructions let technically sophisticated organizations validate or reproduce our output. To use this with Graph Leads, you'll need a hash key issued by robertkittinger@gmail.com.
Apply these rules before hashing. The hash of un-normalized data will not match.
Each value is hashed independently using HMAC-SHA256 with your shared key. Empty values produce an empty string (no hash).
import hmac, hashlib
def make_hash(value: str, key: bytes) -> str:
"""Returns 64-char hex HMAC-SHA256, or '' if value is empty."""
if not value:
return ''
return hmac.new(key, value.encode('utf-8'), hashlib.sha256).hexdigest()
KEY = b"YOUR_HASH_KEY_HERE" # replace with your issued key
# Example
phone_hash = make_hash("2055551234", KEY)
email_hash = make_hash("user@gmail.com", KEY)
The output CSV must have exactly these column names (any extra columns are ignored):
row_index,hash_first,hash_last,hash_full_name,hash_first_last,hash_npi,hash_email,hash_phone,hash_address
row_index = 1-based row number from your source file (for your own tracking).
All other fields are the HMAC hex digest or blank.
Save as hash_contacts.py and run: python hash_contacts.py input.csv output_hashed.csv
#!/usr/bin/env python3
"""
hash_contacts.py — Normalize and hash a contact CSV.
Produces the same output as HashComp (https://hashcomp.com).
Requires: pip install unicodedata2 (or Python 3.3+ built-in unicodedata)
"""
import csv, hmac, hashlib, re, sys, unicodedata
KEY = b"YOUR_HASH_KEY_HERE" # ← replace with key from robertkittinger@gmail.com
CRED_RE = re.compile(
r'\b(dr\.?|mr\.?|mrs\.?|ms\.?|prof\.?|dds|dmd|od|md|do|phd|'
r'pharmd?|rph|pa-?c?|np|rn|lpn|dpt|pt|ot|otd|slp|aud|dc|dpm|'
r'psyd|lcsw|lmft|jr\.?|sr\.?|ii|iii|iv)\b',
flags=re.I
)
ADDR_ABBR = [
(r'\bST\b','STREET'),(r'\bAVE\b','AVENUE'),(r'\bBLVD\b','BOULEVARD'),
(r'\bDR\b','DRIVE'),(r'\bRD\b','ROAD'),(r'\bLN\b','LANE'),
(r'\bCT\b','COURT'),(r'\bPL\b','PLACE'),(r'\bSTE\b','SUITE'),
(r'\bAPT\b','APARTMENT'),(r'\bN\b','NORTH'),(r'\bS\b','SOUTH'),
(r'\bE\b','EAST'),(r'\bW\b','WEST'),
]
def norm_phone(v):
d = re.sub(r'\D','',str(v or ''))
if len(d)==11 and d[0]=='1': d=d[1:]
return d
def norm_email(v):
e = str(v or '').strip().lower()
if '@' in e:
local,dom = e.split('@',1)
local = local.split('+')[0]
e = local+'@'+dom
return e
def norm_name(v):
n = CRED_RE.sub('', str(v or ''))
n = unicodedata.normalize('NFKD',n).encode('ascii','ignore').decode()
return ' '.join(n.upper().split())
def norm_npi(v):
return re.sub(r'\D','',str(v or ''))
def norm_addr(v):
a = str(v or '').upper().strip()
for pat,rep in ADDR_ABBR:
a = re.sub(pat,rep,a)
return ' '.join(a.split())
def h(value):
if not value: return ''
return hmac.new(KEY, value.encode('utf-8'), hashlib.sha256).hexdigest()
def process(in_path, out_path):
# Detect column types from headers
TYPE_KEYWORDS = {
'npi':('npi',), 'email':('email','e-mail'),
'phone':('phone','mobile','cell','tel'),
'first':('first','fname'), 'last':('last','lname','surname'),
'full_name':('full','name','contact'),
'address':('address','street','addr'),
}
with open(in_path, newline='', encoding='utf-8-sig', errors='replace') as f:
reader = csv.DictReader(f)
rows = list(reader)
headers = reader.fieldnames or []
col_map = {}
for hdr in headers:
hl = hdr.lower()
for field, keywords in TYPE_KEYWORDS.items():
if any(k in hl for k in keywords):
col_map[hdr] = field; break
OUT_COLS = ['row_index','hash_first','hash_last','hash_full_name',
'hash_first_last','hash_npi','hash_email','hash_phone','hash_address']
with open(out_path, 'w', newline='', encoding='utf-8') as f:
w = csv.DictWriter(f, fieldnames=OUT_COLS)
w.writeheader()
for i, row in enumerate(rows, 1):
fields = {t: row.get(c,'') for c,t in col_map.items()}
first = norm_name(fields.get('first',''))
last = norm_name(fields.get('last',''))
full = norm_name(fields.get('full_name','') or f'{first} {last}')
fl = first+'_'+last
w.writerow({
'row_index': i,
'hash_first': h(first),
'hash_last': h(last),
'hash_full_name': h(full),
'hash_first_last': h(fl),
'hash_npi': h(norm_npi(fields.get('npi',''))),
'hash_email': h(norm_email(fields.get('email',''))),
'hash_phone': h(norm_phone(fields.get('phone',''))),
'hash_address': h(norm_addr(fields.get('address',''))),
})
print(f"Done. {len(rows)} rows hashed → {out_path}")
if __name__ == '__main__':
if len(sys.argv) < 3:
print("Usage: python hash_contacts.py input.csv output_hashed.csv")
sys.exit(1)
process(sys.argv[1], sys.argv[2])
Download this script: hash_contacts.py · To request your hash key, email robertkittinger@gmail.com.
OpenTDF is an open standard that wraps
any file (including your hashed CSV) in a policy-controlled encryption envelope.
The key idea: you encrypt the hashed file so it can only be decrypted by the intended
recipient (e.g., Graph Leads's server key), and every access is logged to an audit trail.
This adds a second layer of protection on top of HMAC — even if the hashed file is
intercepted in transit, it cannot be opened without the TDF policy key.
TDF is most useful for regulated industries (HIPAA, SOC2) or when the hashed data
itself is considered sensitive. Ask about TDF wrapping when requesting your hash key.
nGrok is a tunneling service for exposing a local server to the internet over a secure HTTPS tunnel. It is not a security technology per se — it is a developer convenience tool. For production use, deploy HashComp on a VPS behind HTTPS (nginx + Let's Encrypt) rather than nGrok. nGrok is useful if you want to quickly test a local instance of hash_contacts.py output against a Graph Leads staging server without a full deploy.