Manual Hashing Instructions

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.

Key requirement: The HMAC-SHA256 algorithm requires a shared secret key. To use your hashed file with Graph Leads or another HashComp partner, both parties must use the same key. Contact robertkittinger@gmail.com to receive your organization's key.

Step 1 — Normalize each field

Apply these rules before hashing. The hash of un-normalized data will not match.

FieldRuleExample inputNormalized output
Phone Remove all non-digits. Strip leading 1 (US country code) if 11 digits. (205) 555-1234
+1-205-555-1234
2055551234
Email Lowercase entire address. Remove +alias before the @. User+Tag@Gmail.COM user@gmail.com
First / Last name Strip titles (Dr., Mr., Mrs.) and credentials (DDS, OD, MD, PhD, etc.). Unicode-fold to ASCII. Uppercase. Collapse spaces. Dr. María José, DDS MARIA JOSE
Full name Same as name — strip credentials, ASCII-fold, uppercase, collapse. DR. BENJAMIN MARK KACOS, OD BENJAMIN MARK KACOS
first_last key Normalize first name + _ + normalize last name (no spaces within each). first: Ben, last: Smith BEN_SMITH
NPI Digits only. NPI: 1234567890 1234567890
Address Uppercase. Expand: ST→STREET, AVE→AVENUE, BLVD→BOULEVARD, DR→DRIVE, RD→ROAD, LN→LANE, CT→COURT, PL→PLACE, STE→SUITE, APT→APARTMENT. Collapse spaces. 123 Main St, Ste 4 123 MAIN STREET SUITE 4

Step 2 — Hash each normalized value

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)

Step 3 — Build the output CSV

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.

Complete Python script

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.

Advanced: TDF encryption & nGrok tunneling

Trusted Data Format (OpenTDF / Virtru)

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

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.

Hash a file online    Request a hash key