Contact Data Hashing Guide

Learn how to properly normalize and hash your contact data to comply with opt-out requests from DoNotContact.net users.

Legal Compliance Required

Failure to properly suppress opt-out users can result in penalties up to $50,120 per email under CAN-SPAM or $500-$1,500 per text under TCPA. Follow this guide exactly to ensure compliance.

Process Overview

1

Normalize Data

Clean and standardize your contact data according to our specifications

2

Apply SHA-256

Generate cryptographic hashes using the SHA-256 algorithm

3

Match & Suppress

Compare hashes and remove matching contacts from your lists

📧 Email Address Normalization

Rules:

  • Convert to lowercase
  • Trim leading and trailing whitespace

Examples:

Input: " USER@EXAMPLE.COM "
Normalized: "user@example.com"
Input: "John.Doe@Gmail.Com"
Normalized: "john.doe@gmail.com"

Code Examples:

Python
import hashlib

def normalize_email(email):
    """Normalize email address for hashing"""
    return email.strip().lower()

def hash_email(email):
    """Generate SHA-256 hash for normalized email"""
    normalized = normalize_email(email)
    return hashlib.sha256(normalized.encode('utf-8')).hexdigest()

# Example usage
email = "  USER@EXAMPLE.COM  "
hash_value = hash_email(email)
print(hash_value)
# Output: b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc2cd8b2f9b338
JavaScript
const crypto = require('crypto');

function normalizeEmail(email) {
    // Normalize email address for hashing
    return email.trim().toLowerCase();
}

function hashEmail(email) {
    // Generate SHA-256 hash for normalized email
    const normalized = normalizeEmail(email);
    return crypto.createHash('sha256').update(normalized, 'utf8').digest('hex');
}

// Example usage
const email = "  USER@EXAMPLE.COM  ";
const hashValue = hashEmail(email);
console.log(hashValue);
// Output: b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc2cd8b2f9b338
PHP
<?php

function normalizeEmail($email) {
    // Normalize email address for hashing
    return strtolower(trim($email));
}

function hashEmail($email) {
    // Generate SHA-256 hash for normalized email
    $normalized = normalizeEmail($email);
    return hash('sha256', $normalized);
}

// Example usage
$email = "  USER@EXAMPLE.COM  ";
$hashValue = hashEmail($email);
echo $hashValue;
// Output: b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc2cd8b2f9b338
?>

📱 Phone Number Normalization

Rules:

  • Remove all non-digit characters (spaces, dashes, parentheses, plus signs)
  • If result is 11 digits and starts with '1', remove the leading '1'
  • Final result should be exactly 10 digits

Examples:

Input: "+1 (555) 123-4567"
Normalized: "5551234567"
Input: "555.123.4567"
Normalized: "5551234567"
Input: "1-555-123-4567"
Normalized: "5551234567"

Code Examples:

Python
import hashlib
import re

def normalize_phone(phone):
    """Normalize phone number for hashing"""
    # Remove all non-digit characters
    digits_only = re.sub(r'\D', '', phone)
    
    # If 11 digits and starts with 1, remove leading 1
    if len(digits_only) == 11 and digits_only.startswith('1'):
        digits_only = digits_only[1:]
    
    # Should be exactly 10 digits
    if len(digits_only) != 10:
        raise ValueError(f"Invalid phone number: {phone}")
    
    return digits_only

def hash_phone(phone):
    """Generate SHA-256 hash for normalized phone"""
    normalized = normalize_phone(phone)
    return hashlib.sha256(normalized.encode('utf-8')).hexdigest()

# Example usage
phone = "+1 (555) 123-4567"
hash_value = hash_phone(phone)
print(hash_value)
# Output: ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d
JavaScript
const crypto = require('crypto');

function normalizePhone(phone) {
    // Remove all non-digit characters
    let digitsOnly = phone.replace(/\D/g, '');
    
    // If 11 digits and starts with 1, remove leading 1
    if (digitsOnly.length === 11 && digitsOnly.startsWith('1')) {
        digitsOnly = digitsOnly.substring(1);
    }
    
    // Should be exactly 10 digits
    if (digitsOnly.length !== 10) {
        throw new Error(`Invalid phone number: ${phone}`);
    }
    
    return digitsOnly;
}

function hashPhone(phone) {
    // Generate SHA-256 hash for normalized phone
    const normalized = normalizePhone(phone);
    return crypto.createHash('sha256').update(normalized, 'utf8').digest('hex');
}

// Example usage
const phone = "+1 (555) 123-4567";
const hashValue = hashPhone(phone);
console.log(hashValue);
// Output: ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d
PHP
<?php

function normalizePhone($phone) {
    // Remove all non-digit characters
    $digitsOnly = preg_replace('/\D/', '', $phone);
    
    // If 11 digits and starts with 1, remove leading 1
    if (strlen($digitsOnly) === 11 && substr($digitsOnly, 0, 1) === '1') {
        $digitsOnly = substr($digitsOnly, 1);
    }
    
    // Should be exactly 10 digits
    if (strlen($digitsOnly) !== 10) {
        throw new Exception("Invalid phone number: " . $phone);
    }
    
    return $digitsOnly;
}

function hashPhone($phone) {
    // Generate SHA-256 hash for normalized phone
    $normalized = normalizePhone($phone);
    return hash('sha256', $normalized);
}

// Example usage
$phone = "+1 (555) 123-4567";
$hashValue = hashPhone($phone);
echo $hashValue;
// Output: ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d
?>

🔄 Complete Suppression Example

Here's a complete example showing how to process a CSV file from DoNotContact.net:

Sample CSV from DoNotContact.net:

email_hash,phone_hash,opt_out_id,submission_date
b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc2cd8b2f9b338,ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d,OPT001,2025-01-15T10:30:00Z
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456,f6e5d4c3b2a1098765432109876543210fedcba0987654321fedcba098765,OPT002,2025-01-15T11:45:00Z

Python Suppression Script:

suppression_script.py
import hashlib
import csv
import re

def normalize_email(email):
    """Normalize email address for hashing"""
    return email.strip().lower()

def normalize_phone(phone):
    """Normalize phone number for hashing"""
    digits_only = re.sub(r'\D', '', phone)
    if len(digits_only) == 11 and digits_only.startswith('1'):
        digits_only = digits_only[1:]
    if len(digits_only) != 10:
        raise ValueError(f"Invalid phone number: {phone}")
    return digits_only

def hash_contact(contact, contact_type):
    """Hash a contact (email or phone)"""
    if contact_type == 'email':
        normalized = normalize_email(contact)
    elif contact_type == 'phone':
        normalized = normalize_phone(contact)
    else:
        raise ValueError("contact_type must be 'email' or 'phone'")
    
    return hashlib.sha256(normalized.encode('utf-8')).hexdigest()

def load_suppression_hashes(csv_file):
    """Load suppression hashes from DoNotContact.net CSV"""
    email_hashes = set()
    phone_hashes = set()
    
    with open(csv_file, 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            if row['email_hash']:
                email_hashes.add(row['email_hash'])
            if row['phone_hash']:
                phone_hashes.add(row['phone_hash'])
    
    return email_hashes, phone_hashes

def suppress_contacts(contact_list, suppression_csv):
    """Suppress contacts based on DoNotContact.net hashes"""
    email_hashes, phone_hashes = load_suppression_hashes(suppression_csv)
    
    suppressed_contacts = []
    remaining_contacts = []
    
    for contact in contact_list:
        should_suppress = False
        
        # Check email hash
        if 'email' in contact and contact['email']:
            try:
                email_hash = hash_contact(contact['email'], 'email')
                if email_hash in email_hashes:
                    should_suppress = True
                    print(f"Suppressing email: {contact['email']}")
            except Exception as e:
                print(f"Error hashing email {contact['email']}: {e}")
        
        # Check phone hash
        if 'phone' in contact and contact['phone'] and not should_suppress:
            try:
                phone_hash = hash_contact(contact['phone'], 'phone')
                if phone_hash in phone_hashes:
                    should_suppress = True
                    print(f"Suppressing phone: {contact['phone']}")
            except Exception as e:
                print(f"Error hashing phone {contact['phone']}: {e}")
        
        if should_suppress:
            suppressed_contacts.append(contact)
        else:
            remaining_contacts.append(contact)
    
    return remaining_contacts, suppressed_contacts

# Example usage
if __name__ == "__main__":
    # Sample contact list
    contacts = [
        {"email": "  USER@EXAMPLE.COM  ", "phone": "+1 (555) 123-4567", "name": "John Doe"},
        {"email": "jane@company.com", "phone": "555-987-6543", "name": "Jane Smith"},
        {"email": "test@domain.org", "phone": "1-800-555-0199", "name": "Test User"}
    ]
    
    # Suppress contacts using DoNotContact.net CSV
    remaining, suppressed = suppress_contacts(contacts, "donotcontact_suppressions.csv")
    
    print(f"\nOriginal contacts: {len(contacts)}")
    print(f"Suppressed: {len(suppressed)}")
    print(f"Remaining: {len(remaining)}")
    
    # Save cleaned list
    with open('cleaned_contacts.csv', 'w', newline='') as f:
        if remaining:
            writer = csv.DictWriter(f, fieldnames=remaining[0].keys())
            writer.writeheader()
            writer.writerows(remaining)

Best Practices & Security

✅ Do:

  • Use UTF-8 encoding for all text processing
  • Validate phone numbers are exactly 10 digits after normalization
  • Process suppressions immediately upon receipt
  • Keep audit logs of suppression activities
  • Test your hashing implementation with known values

❌ Don't:

  • Skip email normalization (case matters!)
  • Use different hashing algorithms
  • Add salt or other modifications to the hash
  • Ignore suppression requests or delay processing
  • Store or log raw contact data during processing

🔍 Verify Your Implementation

Use these test cases to verify your normalization and hashing implementation:

Test Cases:

Email: " TEST@DOMAIN.COM "
Expected Hash: 1592775cfdf6b3db59c30a7c0b5ce6e76b93fc0fbac9fb5ce7ff4b5e02cf9bc0
Phone: "+1 (555) 123-4567"
Expected Hash: ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d
Phone: "555.987.6543"
Expected Hash: 3ba0c5edd8e5ba3cf7b6b7ce20e9acbdcd80dd91bb79dbb1b2b3cf3d7de1b7e6

Need Help?

If you're having trouble implementing this hashing guide or need technical assistance, we're here to help.