Validate email addresses in Flask
To validate an email in Flask, call the SimpleVerifier API from your route or a Flask-WTF validator and reject the address only when the status is invalid. Keep the key in an environment variable, run a format check before the API call, and accept the address if the request times out.
By SimpleVerifier Team
Before you start
- Create a key on the API keys page. Keys start with
sv_live_and are shown once. - Set
SIMPLEVERIFIER_KEYin the environment your WSGI server runs in (systemd unit, Docker env, or your platform's settings). - API access is included in the Pro plan ($29.99/month) and the Email + Phone plan ($49.99/month). See pricing.
1. Add the helper
This is the same helper as the Python guide. Save it next to your app and install requests.
# simpleverifier.py — Python 3.10+, needs `requests`
import logging
import os
import re
import requests
API_URL = "https://simpleverifier.com/api/v1/verify"
EMAIL_SHAPE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
log = logging.getLogger(__name__)
def email_status(email: str) -> str | None:
"""Return "valid", "risky" or "invalid", or None if the API could not answer."""
email = email.strip()
if not EMAIL_SHAPE.match(email):
return "invalid" # fails the local syntax check, no API call made
try:
r = requests.get(
API_URL,
params={"email": email},
headers={"Authorization": f"Bearer {os.environ['SIMPLEVERIFIER_KEY']}"},
timeout=(5, 30), # connect, read
)
except requests.RequestException as exc:
log.warning("SimpleVerifier unreachable: %s", exc)
return None
if r.status_code != 200:
log.warning("SimpleVerifier returned HTTP %s", r.status_code)
return None # 401, 402, 429, 503: fail open
return r.json().get("status")2. Check the address in a route
Return 422 for an undeliverable address so your front end can show the message next to the field.
# app.py — Flask 2.0+; simpleverifier.py is the helper shown above
from flask import Flask, jsonify, request
from simpleverifier import email_status
app = Flask(__name__)
@app.post("/signup")
def signup():
email = (request.form.get("email") or "").strip()
status = email_status(email)
if status == "invalid":
return jsonify(error="This email address cannot receive mail. Please check it."), 422
# status is "valid", "risky" or None (API unavailable): accept the signup.
# Save it here, keeping status if you want to review risky addresses later.
return jsonify(ok=True, email_status=status), 2013. Or as a Flask-WTF validator
WTForms runs every validator in the list and collects errors, so the custom validator checks field.errors and skips the API call when the format check already failed.
# forms.py — Flask-WTF (the Email() validator needs the email-validator package)
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email, ValidationError
from simpleverifier import email_status
def deliverable(form, field):
if field.errors:
return # Email() already rejected the syntax, so skip the API call
if email_status(field.data) == "invalid":
raise ValidationError("This email address cannot receive mail. Please check it.")
class SignupForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email(), deliverable])What the API returns
GET or POST https://simpleverifier.com/api/v1/verify with email runs the syntax, domain, disposable-domain and live mailbox checks in one call and returns:
{
"email": "jane@company.com",
"status": "valid",
"reason": "Mailbox exists",
"mailbox_checked": true
}Every snippet above follows the same rule: only a successful response with status set to invalid blocks a person.
| Result | What it means | What to do |
|---|---|---|
| valid | The mail server confirmed the mailbox exists. | Accept. |
| risky | The domain is real but the mailbox couldn't be confirmed: catch-all domains, role addresses like info@, providers that hide mailbox status. | Accept on signup forms. Store the status and keep these out of cold campaigns. |
| invalid | Bad format, no mail server, a disposable domain, or the mailbox doesn't exist. | Reject and ask the person to check the address. |
| timeout, 429 or 503 | The check didn't finish: a slow mail server, too many calls this minute, or a brief outage. | Accept (fail open) and log it. Re-check later if it matters. |
| 401 or 402 | The key is wrong or revoked, or the subscription isn't active. | Accept, and alert yourself: every check is being skipped until it's fixed. |
Errors return JSON with an error message. Each key can make 120 calls a minute; for thousands of addresses, upload a CSV in the dashboard instead.
Phone numbers and spreadsheets
The same key works with /api/v1/phone?phone=…&country=… to validate a phone number and normalise it to E.164, with live carrier lookups on the Email + Phone plan. The request and response are in the API reference. To check a column of addresses without code, use the Google Sheets add-on with the same key.
Common questions
- Does this work with Quart or async Flask views?
- The helper uses requests, which blocks. In an async view, either run it with asyncio.to_thread(email_status, email) or swap requests for httpx.AsyncClient with the same URL, header and timeout.
- Where should I store the result?
- Saving the status next to the user (valid, risky or empty) is useful later: you can leave risky addresses out of cold campaigns, and re-check the empty ones when the API is back.
- How much does the SimpleVerifier API cost?
- API access is included in the paid plans: Pro at $29.99/month and Email + Phone at $49.99/month. There are no per-call credits. Each key can make 120 calls a minute.
- Should I reject risky addresses?
- Not on a signup or contact form. Risky means the mailbox couldn't be confirmed, which includes every address at a catch-all company domain. Accept it, store the status, and leave risky addresses out of cold outreach.
- What if the API is slow or down?
- Let the address through. A mailbox check can take several seconds when a mail server is slow, so cap the wait at about 30 seconds (less on a busy form) and treat a timeout, a 429 or a 503 as unknown rather than invalid.