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
Normalize Data
Clean and standardize your contact data according to our specifications
Apply SHA-256
Generate cryptographic hashes using the SHA-256 algorithm
Match & Suppress
Compare hashes and remove matching contacts from your lists
📧 Email Address Normalization
Rules:
- Convert to lowercase
- Trim leading and trailing whitespace
Examples:
Code Examples:
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
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
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:
Code Examples:
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
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
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:
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:
Need Help?
If you're having trouble implementing this hashing guide or need technical assistance, we're here to help.