Prerequisites
You need three things:
- Python 3.8 or newer.
- The
requestslibrary. - A free API Ninjas API key — issued instantly, no credit card. If you already have an account, it's on your profile page.
pip install requestsStep 1: Validate your first address
The endpoint is /v1/validateemail and takes one parameter: email.
import requests
response = requests.get(
'https://api.api-ninjas.com/v1/validateemail',
params={'email': 'test@example.com'},
headers={'X-Api-Key': 'YOUR_API_KEY'},
timeout=10,
)
response.raise_for_status()
result = response.json()
print('valid' if result['is_valid'] else 'invalid')The request on the wire and the full response:
{
"email": "test@example.com",
"is_valid": true,
"is_disposable": false,
"mx_found": true,
"smtp_check": true
}Step 2: Read the four flags
Each flag answers a different question, from cheapest check to deepest:
| Flag | Question it answers |
|---|---|
is_valid | Is the address syntactically well-formed (RFC 5322)? |
mx_found | Does the domain have mail servers (MX records) at all? |
is_disposable | Is the domain a known throwaway provider (mailinator, 10minutemail)? |
smtp_check | Does the domain's mail server respond to an SMTP probe? |
No email is ever sent during validation — these are syntax, DNS, and server checks. That means a passing result guarantees the domain can receive mail, not that a particular inbox exists; it's the practical ceiling for any validator.
Step 3: Build a safe-to-send check
In practice you want one boolean, and it should combine three flags: well-formed, deliverable domain, not disposable. Here's the client with that helper — key in an environment variable, one shared session:
import os
import requests
API_URL = 'https://api.api-ninjas.com/v1/validateemail'
session = requests.Session()
session.headers['X-Api-Key'] = os.environ.get('API_NINJAS_KEY', 'YOUR_API_KEY')
def validate_email(email: str) -> dict:
response = session.get(API_URL, params={'email': email}, timeout=10)
response.raise_for_status()
return response.json()
def is_safe_to_send(email: str) -> bool:
"""True only if the address is well-formed, deliverable, and not a throwaway."""
result = validate_email(email)
return (
result['is_valid']
and result.get('mx_found', False)
and not result.get('is_disposable', False)
)
for email in ['user@gmail.com', 'fake@notarealdomain.xyz', 'user@mailinator.com']:
print(f"{'OK ' if is_safe_to_send(email) else 'NO '} {email}")Whether to also require smtp_check is a strictness dial: some legitimate mail servers decline probes, so requiring it trades a few false rejections for a cleaner list.
Step 4: Wire it into a sign-up form
The highest-value place for validation is registration, server-side, with a specific message per failure so users can actually fix their typo:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/signup', methods=['POST'])
def signup():
email = request.json.get('email', '')
result = validate_email(email)
if not result['is_valid']:
return jsonify({'error': 'Please enter a valid email address'}), 400
if not result.get('mx_found'):
return jsonify({'error': 'This email domain cannot receive mail'}), 400
if result.get('is_disposable'):
return jsonify({'error': 'Disposable email addresses are not allowed'}), 400
# ...create the account...
return jsonify({'ok': True})Keep a simple regex on the client for instant feedback, but treat this server-side check as the real gate — anything running in the browser can be bypassed.
Step 5: Clean a whole list
For an existing mailing list, run every address through the same client and split the results. Bounced sends damage sender reputation, so this pays for itself before the next campaign:
with open('emails.txt') as f:
emails = [line.strip() for line in f if line.strip()]
valid, invalid = [], []
for email in emails:
result = validate_email(email)
(valid if result['is_valid'] else invalid).append(email)
print(f'Valid: {len(valid)}')
print(f'Invalid: {len(invalid)}')
with open('emails_clean.txt', 'w') as f:
f.write('\n'.join(valid))For lists beyond a few thousand, add a short time.sleep() between calls to stay inside your plan's rate limit. And when something does go wrong, handle it per-address so one failure doesn't stop the run:
from requests.exceptions import HTTPError, RequestException
try:
result = validate_email('user@gmail.com')
except HTTPError as error:
status = error.response.status_code
if status == 401:
print('Missing or invalid API key')
elif status == 429:
print('Rate limit exceeded — throttle the loop')
else:
print(f'HTTP {status}: {error.response.text}')
except RequestException as error:
print(f'Network error: {error}')Where to go from here
Contact hygiene rarely stops at email: the Validate Phone API does the same job for phone numbers, the Disposable Email Checker isolates the throwaway test, and the MX Lookup API exposes the raw DNS records. The complete script is below.