Real-Time Email Validation: From API Call to Production Deployment
I was working on a SaaS product that sent welcome emails to new users. Everything seemed fine until our infrastructure team noticed something alarming: our bounce rate had climbed to 8.7%. They warned me that ISPs flag senders with bounce rates above 5% as spam, and we were heading toward the spam folder on Gmail and Outlook.
When we investigated, we discovered the root cause. Our signup form validated emails with a simple regex pattern, and we were accepting everything from disposable email services to typos that looked syntactically correct. One user had signed up with test@gmai1.com (with a "1" instead of "l"), and we sent three welcome emails to a non-existent address before giving up. Multiply that by thousands of users, and you've poisoned your sender reputation.
That's when I learned that real email validation is far more complex than checking format. Today, I'll show you how to build a production-grade validation system that catches these problems before they destroy your deliverability.
Why Regex Validation Is Just The Beginning
Let's start with why your current approach probably isn't working. Most developers rely on regex patterns like this:
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function isValidEmail(email) {
return emailRegex.test(email);
}
// Test some addresses
console.log(isValidEmail("john@example.com")); // true
console.log(isValidEmail("test@tempmail.com")); // true (WRONG!)
console.log(isValidEmail("user@typo.cmo")); // true (WRONG!)
console.log(isValidEmail("info@non-existent.xyz")); // true (WRONG!)
This regex is only checking syntax—whether the string looks like an email address. It doesn't care if the domain actually exists, if the mailbox accepts mail, or whether you're inviting a spam bot into your system.
The real damage happens slowly. Each invalid email you send out contributes to your bounce rate. When your bounce rate climbs past 5%, ISPs start treating you like a spammer. Some Gmail accounts stop receiving your emails entirely. Your support team gets flooded with users saying they never received your welcome email. It's a cascading failure that's expensive to fix.
I ran the numbers at my company: by the time we fixed the problem, we'd wasted roughly $12,000 on email delivery services sending to dead addresses, and we'd lost an estimated 40 sign-ups from users who assumed we had a broken system when they didn't receive confirmations.
The solution is to validate emails properly before you ever send them. This means checking not just format, but also whether the address actually exists and whether it's legitimate.
Building a Real Email Validator With BillionVerify
Let me show you how to build a proper validator using the BillionVerify API. We'll start with Node.js, then move to Python. The key difference from a regex checker is that we're actually hitting an API that validates against real mail servers.
Here's the basic approach: when a user enters their email, we send it to BillionVerify, which performs SMTP validation against the recipient's mail server. Within milliseconds, we get back detailed information about whether the email is valid, whether it's a disposable address, whether it's a catch-all account, and whether it's a known spam trap.
Let's build this step by step. First, we'll create a simple wrapper around the BillionVerify API that handles the HTTP request and processes the response:
const https = require('https');
class BillionVerifyValidator {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'api.billionverify.com';
this.requestTimeout = 5000; // 5 seconds
}
validateEmail(email) {
return new Promise((resolve, reject) => {
// Build the JSON request body
const requestBody = JSON.stringify({
email: email,
check_smtp: true
});
// Configure HTTPS request
const options = {
hostname: this.baseUrl,
path: '/v1/verify/single',
method: 'POST',
headers: {
'BV-API-KEY': this.apiKey,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody),
'User-Agent': 'MyApp-EmailValidator/1.0',
'Connection': 'keep-alive'
}
};
// Make the request with a timeout
const req = https.request(options, (res) => {
let responseData = '';
// Accumulate response chunks
res.on('data', (chunk) => {
responseData += chunk;
});
// Process complete response
res.on('end', () => {
try {
const result = JSON.parse(responseData);
// Validate response structure
if (!result.hasOwnProperty('status')) {
reject(new Error('Invalid API response: missing status field'));
return;
}
resolve(result);
} catch (error) {
reject(new Error(`Failed to parse API response: ${error.message}`));
}
});
});
// Handle request errors
req.on('error', (error) => {
reject(new Error(`API request failed: ${error.message}`));
});
// Handle timeout
req.on('timeout', () => {
req.destroy();
reject(new Error('API request timed out after 5 seconds'));
});
// Set timeout, send request body, and finish
req.setTimeout(this.requestTimeout);
req.write(requestBody);
req.end();
});
}
}
// Now let's use this validator in a realistic scenario
async function validateUserSignup(userEmail) {
try {
const validator = new BillionVerifyValidator(process.env.BILLIONVERIFY_API_KEY);
const result = await validator.validateEmail(userEmail);
console.log(`Validation result for ${userEmail}:`);
console.log(` Status: ${result.status}`);
console.log(` Disposable: ${result.is_disposable}`);
console.log(` Catch-all: ${result.is_catch_all}`);
console.log(` Spam trap: ${result.is_spam_trap}`);
console.log(` Role account: ${result.is_role_account}`);
// Business logic: decide whether to accept this email
if (result.status === 'valid' && !result.is_disposable && !result.is_spam_trap) {
return { accepted: true, message: 'Email validated successfully' };
} else if (result.is_disposable) {
return { accepted: false, message: 'Please use a permanent email address, not a temporary one' };
} else if (result.is_spam_trap) {
return { accepted: false, message: 'This email address cannot be used' };
} else {
return { accepted: false, message: 'This email address appears to be invalid' };
}
} catch (error) {
console.error(`Validation error: ${error.message}`);
// In production, you might have a fallback strategy here
return { accepted: false, message: 'Email validation service temporarily unavailable' };
}
}
// Example usage
validateUserSignup('john@example.com').then(result => {
console.log(result);
});
Notice how we're handling several important things here that go beyond the basic API call. We're validating the response structure, handling timeouts gracefully, catching errors properly, and then applying business logic to decide whether to accept the email. The response from BillionVerify includes multiple flags—is_disposable, is_spam_trap, is_catch_all—and as the developer, you decide how strictly to enforce these rules for your use case.
The Python Approach: A More Structured Implementation
Now let's look at how you'd implement the same logic in Python, but with a slightly different approach that takes advantage of Python's strengths. Python's requests library makes HTTP calls simpler, and we can use type hints and classes to make the code more maintainable:
import requests
import os
import json
from typing import Dict, Optional, Tuple
from datetime import datetime
class EmailValidator:
"""
Validates email addresses using the BillionVerify API.
This validator checks not just email format, but also whether the
email address actually exists and whether it's legitimate (not disposable,
not a spam trap, etc.).
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = 'https://api.billionverify.com/v1'
# Use a session for connection pooling (more efficient)
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'MyApp-EmailValidator/1.0'
})
def validate(self, email: str) -> Dict:
"""
Validate a single email address.
Makes a request to the BillionVerify API and returns detailed
validation information including whether the email is valid,
disposable, a catch-all, etc.
Args:
email: The email address to validate
Returns:
Dictionary containing:
- status: 'valid', 'invalid', 'unknown', 'undeliverable'
- is_disposable: True if it's a temporary email service
- is_catch_all: True if the domain accepts all addresses
- is_spam_trap: True if it's a known spam trap
- is_role_account: True if it's a generic account (info@, admin@, etc.)
Raises:
ConnectionError: If the API request fails
ValueError: If the response is malformed
"""
try:
# Make the API request
response = self.session.get(
f'{self.base_url}/verify',
params={'email': email, 'key': self.api_key},
timeout=5
)
# Check HTTP status code
if response.status_code == 401:
raise ValueError('Invalid API key')
elif response.status_code == 429:
raise ConnectionError('Rate limited: too many requests')
elif response.status_code >= 500:
raise ConnectionError('BillionVerify API server error')
response.raise_for_status()
# Parse and return the JSON response
result = response.json()
return result
except requests.exceptions.Timeout:
raise ConnectionError('API request timed out after 5 seconds')
except requests.exceptions.ConnectionError as e:
raise ConnectionError(f'Failed to connect to API: {str(e)}')
except json.JSONDecodeError:
raise ValueError('API returned invalid JSON')
def should_accept_for_signup(self, email: str) -> Tuple[bool, Optional[str]]:
"""
Determines whether an email should be accepted for user signup.
This is where you apply your business rules. Different applications
might have different policies. For example:
- A free tier might accept disposable emails, but a business tier might not
- Some apps might allow catch-all addresses, others might reject them
- Most should reject known spam traps
Args:
email: Email address to evaluate
Returns:
Tuple of (accepted: bool, reason: Optional[str])
If accepted is False, reason contains why the email was rejected
"""
try:
result = self.validate(email)
except Exception as e:
# If validation fails, we have a choice: fail open or fail closed
# Failing open means accepting the user anyway
# Failing closed means rejecting the user
# This depends on your product requirements
print(f'Warning: validation error for {email}: {str(e)}')
return False, 'Email validation service temporarily unavailable'
# Check the validation result
if result['status'] != 'valid':
return False, 'This email address does not exist'
if result.get('is_spam_trap'):
# Never accept spam traps
return False, 'This email address is not valid'
if result.get('is_disposable'):
# For this application, we require permanent email addresses
return False, 'Please use a permanent email address, not a temporary one'
# All checks passed
return True, None
def validate_batch(self, emails: list) -> Dict[str, Dict]:
"""
Validate multiple email addresses.
This is useful for cleaning up email lists before a mailing campaign.
Note that this makes one API call per email, so for large lists
you might want to implement batching or caching.
Args:
emails: List of email addresses
Returns:
Dictionary mapping email -> validation result
"""
results = {}
for email in emails:
try:
results[email] = self.validate(email)
except Exception as e:
results[email] = {'error': str(e), 'status': 'error'}
return results
# Example: Signup flow
def handle_user_signup(email: str, password: str) -> Tuple[bool, str]:
"""
Example signup function that uses email validation.
"""
validator = EmailValidator(os.getenv('BILLIONVERIFY_API_KEY'))
# Validate the email address
accepted, reason = validator.should_accept_for_signup(email)
if not accepted:
return False, reason
# If we get here, the email is valid and meets our requirements
# Now proceed with account creation
print(f'Email {email} validated. Creating user account...')
# (Rest of signup logic would go here)
return True, 'Account created successfully'
# Example usage
if __name__ == '__main__':
# Test the validator
test_emails = [
'john@example.com',
'test@tempmail.com',
'admin@mycompany.com',
'user@unregistered-domain.fake'
]
validator = EmailValidator(os.getenv('BILLIONVERIFY_API_KEY'))
for email in test_emails:
accepted, reason = validator.should_accept_for_signup(email)
print(f'\n{email}:')
print(f' Accepted: {accepted}')
if reason:
print(f' Reason: {reason}')
This Python implementation is more structured than the Node.js version. We're using type hints to make the code self-documenting, we're building business logic directly into the validator class, and we're handling different types of errors separately. Notice how the should_accept_for_signup method encapsulates all the business rules—you can easily modify what gets accepted by changing the logic there.
Handling Real-World Scenarios: Caching and Performance
The approach above works, but if you're processing thousands of emails, you'll hit performance problems and API rate limits. That's why we need caching. The key insight is that the validation result for an email address is stable—if john@example.com is valid today, it will be valid tomorrow. So we can cache results and only validate new addresses.
Here's how to implement intelligent caching with Redis:
import redis
import hashlib
import json
from typing import Dict, Optional
class CachedEmailValidator(EmailValidator):
"""
Extended validator that caches results using Redis.
This dramatically improves performance for applications that see
duplicate email validations (which is most of them—many users reuse
the same company email addresses).
"""
def __init__(self, api_key: str, redis_host: str = 'localhost', redis_port: int = 6379):
super().__init__(api_key)
self.redis = redis.Redis(host=redis_host, port=redis_port, decode_responses=True)
self.cache_ttl = 86400 * 7 # Cache for 7 days
def _get_cache_key(self, email: str) -> str:
"""Generate a consistent cache key for an email address."""
# Use MD5 hash to keep keys reasonably short
email_hash = hashlib.md5(email.lower().encode()).hexdigest()
return f'email_validation:{email_hash}'
def validate(self, email: str) -> Dict:
"""
Validate email, using cache when available.
This override checks Redis cache first. If we have a recent result,
we return it immediately without calling the API. If not, we call
the API and cache the result for future lookups.
"""
cache_key = self._get_cache_key(email)
# Check cache first
cached_result = self.redis.get(cache_key)
if cached_result:
result = json.loads(cached_result)
result['cached'] = True # Mark this as coming from cache
return result
# Cache miss: call the API
result = super().validate(email)
# Store in cache with TTL
self.redis.setex(cache_key, self.cache_ttl, json.dumps(result))
result['cached'] = False
return result
# Example: Validating a signup form with caching
class SignupService:
"""Service that handles user signups with cached email validation."""
def __init__(self, api_key: str):
# This validator will cache results automatically
self.validator = CachedEmailValidator(api_key)
def register_user(self, email: str, password: str, name: str) -> Dict:
"""
Register a new user with full validation.
The first time someone with this email address signs up, we call
the API. If someone else with the same email tries to sign up later
that same week, we use the cached result—much faster.
"""
# Validate email (may use cache)
result = self.validator.validate(email)
# Check if we should accept this email
if result['status'] != 'valid':
return {
'success': False,
'error': 'Email address does not exist',
'field': 'email'
}
if result.get('is_disposable'):
return {
'success': False,
'error': 'Please use a permanent email address',
'field': 'email'
}
# Email is valid, proceed with user creation
# (In real code, you'd save to database here)
return {
'success': True,
'user_id': '12345',
'email': email,
'name': name,
'validation_cached': result.get('cached', False)
}
# Performance comparison example
if __name__ == '__main__':
import time
service = SignupService(os.getenv('BILLIONVERIFY_API_KEY'))
# First signup: will hit the API
print('First signup (no cache):')
start = time.time()
result1 = service.register_user('alice@example.com', 'password123', 'Alice')
elapsed1 = time.time() - start
print(f' Completed in {elapsed1:.3f} seconds')
print(f' Cached: {result1.get("validation_cached", False)}')
# Second signup with same email: will use cache
print('\nSecond signup (same email, cached):')
start = time.time()
result2 = service.register_user('alice@example.com', 'different_password', 'Alice Smith')
elapsed2 = time.time() - start
print(f' Completed in {elapsed2:.3f} seconds')
print(f' Cached: {result2.get("validation_cached", False)}')
print(f' Speedup: {elapsed1/elapsed2:.1f}x faster')
The caching approach is crucial for production systems. In my experience, about 70-80% of email validations can be served from cache, which means the average response time drops from 300ms to under 10ms. For a signup flow, that's the difference between feeling responsive and feeling sluggish.
Why This Matters in Production
Let me walk you through what actually happens when you skip proper email validation. I've seen this play out at several companies:
The Scenario: Your product sends transactional emails (welcome emails, password resets, notifications). You have 50,000 users. Unbeknownst to you, about 8% of those emails are invalid—a mix of typos, disposable addresses, and spam traps.
Week 1: You send out a feature announcement email. 4,000 of your 50,000 emails bounce. Your email provider logs this.
Week 2: You send another batch. Same problem. Your mail server's IP reputation starts to decline.
Week 3: Gmail and Outlook flag your emails as spam. Not all of them—just a percentage at first. But it's growing. Your bounce rate is now 8%, well above the 5% safety threshold.
Week 4: Major ISPs start filtering your emails to the spam folder by default. Legitimate users stop receiving your emails. They think your product is broken. Support tickets flood in. You've poisoned your sender reputation.
The Fix: Validate emails before you add them to your list. That 8% becomes 0-1% bounces. Your sender reputation stays clean. Emails arrive reliably in the inbox. Users have a good experience.
The BillionVerify API does this validation in milliseconds. It's real-time, accurate, and with proper caching, it's also cost-effective.
Getting Started
Ready to build this into your product? Here's what you need to do:
First, sign up for BillionVerify at https://billionverify.com/auth/sign-up. You'll get 100 free credits to test with, no credit card required.
Then grab your API key from the dashboard and set it as an environment variable:
export BILLIONVERIFY_API_KEY="your_api_key_here"
Now take one of the code examples above (choose Node.js or Python depending on your stack) and integrate it into your signup flow. Start by just logging the results—don't reject emails yet. This way you can see what your current user base looks like. After a week, you'll have good data on how many disposable addresses, catch-all domains, and spam traps your users are signing up with.
Once you see the data, you can decide which rules to enforce. Most applications reject spam traps immediately. Many reject disposable emails. Some allow catch-all domains, others don't. It depends on your business model.
The code examples in this article are complete and ready to use. Copy them, modify the business logic to match your requirements, and deploy. Your bounce rates will thank you.
Check out the full API documentation at https://billionverify.com/docs for details on all the fields in the response and advanced options like batch validation and list cleaning.
Have questions about implementing this in your stack? Drop them in the comments and I'll help you troubleshoot.
Top comments (0)