Blog / Integration

Python Email Verification: Complete Guide to the MailValid API for Developers

Your Python application is silently accumulating invalid email addresses, and every bad address stored costs you money in bounce fees, damages your sender reputation, and erodes the deliverability of your entire email pipeline. According to Validity's 2023 State of Email Deliverability report, the average email list degrades by 22.5% annually as people abandon addresses, change jobs, or submit disposable emails. If you are building a SaaS signup flow, processing leads from a CRM, or running a transactional email system in Python, email verification is not optional — it is a foundational requirement that determines whether your emails reach the inbox or get flagged as spam.

This guide covers everything you need to integrate the MailValid API into a Python application: single email verification, bulk processing with async I/O, webhook callbacks, and production-ready patterns for Django, Flask, and FastAPI. Every code example is copy-paste ready.


Why Python Developers Need Email Verification (Not Just Regex)

The most common mistake Python developers make is relying on regex-based email validation. A pattern like r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' will accept [email protected] without complaint — even though the domain does not exist. Regex checks syntax, not deliverability.

According to HubSpot's 2024 Email Marketing Report, 28% of email addresses submitted through web forms contain typos, syntax errors, or completely fabricated domains. Meanwhile, Litmus research shows that each 1% increase in bounce rates correlates with a 10% decrease in inbox placement rates over a 30-day period.

Real email verification goes beyond syntax. It checks four layers simultaneously:

  1. Syntax validation — RFC 5322 format compliance
  2. DNS/MX lookup — does the domain accept email?
  3. SMTP handshake — does the mailbox actually exist?
  4. Reputation signals — disposable, role-based, or catch-all detection

The MailValid API performs all four checks in a single request, returning a confidence score and detailed status flags. This is what separates a real email verification API from a regex function. For a deeper look at how email syntax parsing works at the RFC level, see our guide on decoding email address parsing and RFC standards.

Email verification API pipeline showing Python connecting to verification service with syntax, DNS, SMTP, and reputation checks

The business case is clear. According to Twilio SendGrid's 2024 Global Email Benchmark Report, companies that implement real-time email verification at signup see a 34% reduction in hard bounces within the first 90 days. Validity's Return Path data shows that senders with bounce rates above 2% see inbox placement drop by 11% on average. And DataValidation.com's analysis of 2.5 billion email addresses found that 18.3% of all collected emails become invalid within one year — meaning even verified lists need periodic re-cleaning.


Setting Up the MailValid Python Client

Getting started with the MailValid API in Python takes about two minutes. You need an API key (sign up at mailvalid.io to get one with the mv_live_ prefix) and the requests library — which comes with most Python installations.

Install the Dependencies

pip install requests

That is it. The MailValid API uses standard HTTP/JSON, so there is no SDK to install. If you prefer async (recommended for bulk processing), add aiohttp:

pip install aiohttp

Your First Verification Request

Here is the most basic integration — verify a single email address in Python:

import requests

response = requests.post( "https://mailvalid.io/api/v1/verify", headers={"X-API-Key": "mv_live_your_key_here"}, json={"email": "[email protected]"} ) result = response.json() print(result)

{

"email": "[email protected]",

"status": "valid",

"score": 0.95,

"checks": {

"syntax_valid": true,

"domain_exists": true,

"mx_found": true,

"smtp_reachable": true,

"disposable": false,

"role_based": false,

"catch_all": false

},

"suggestion": null,

"provider": "google"

}

The status field returns one of four values: valid, invalid, risky, or unknown. The score field is a confidence value from 0.0 to 1.0. Anything above 0.7 is safe to send to; anything below 0.3 should be discarded.

[!TIP] Always check the status field, not just the HTTP status code. A 200 response can still contain an invalid email — the API succeeded, but the email did not.


Production-Ready Patterns for Python Applications

A single verification request works fine for testing, but production applications need error handling, retries, rate limiting awareness, and structured logging. Here are the patterns that matter.

Robust Single Verification with Error Handling

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry

class MailValidClient: BASE_URL = "https://mailvalid.io/api/v1"

def __init__(self, api_key: str, timeout: int = 10):
    self.api_key = api_key
    self.timeout = timeout
    self.session = requests.Session()

    # Retry on 5xx and connection errors
    retries = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[500, 502, 503, 504]
    )
    self.session.mount("https://", HTTPAdapter(max_retries=retries))

def verify(self, email: str) -> dict:
    """Verify a single email address."""
    try:
        response = self.session.post(
            f"{self.BASE_URL}/verify",
            headers={
                "X-API-Key": self.api_key,
                "Content-Type": "application/json"
            },
            json={"email": email},
            timeout=self.timeout
        )
        response.raise_for_status()
        return response.json()
    except requests.exceptions.Timeout:
        return {"email": email, "status": "unknown", "error": "timeout"}
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 429:
            return {"email": email, "status": "unknown", "error": "rate_limited"}
        raise
    except requests.exceptions.ConnectionError:
        return {"email": email, "status": "unknown", "error": "connection_failed"}

Usage

client = MailValidClient(api_key="mv_live_your_key_here") result = client.verify("[email protected]") print(f"{result['email']}: {result['status']} (score: {result.get('score', 'N/A')})")

[email protected]: invalid (score: 0.12)

Checking the Response Structure

Every MailValid response follows a consistent schema. Here is what each field means for your application logic:

FieldTypeDescription
emailstringThe verified email (normalized lowercase)
statusstringvalid | invalid | risky | unknown
scorefloatConfidence score 0.0–1.0
checks.syntax_validboolPasses RFC 5322 syntax rules
checks.domain_existsboolDNS A/MX records found
checks.smtp_reachableboolSMTP server accepts the address
checks.disposableboolTemporary/disposable email provider
checks.role_basedboolGeneric role address (info@, admin@)
checks.catch_allboolDomain accepts all addresses
suggestionstring?Typo correction (e.g., "gmail.com")
providerstringDetected email provider

Bulk Email Verification with Async Python

When you need to verify thousands of emails — during a CRM import, a list cleanup, or a data migration — sequential requests are too slow. A single SMTP check takes 200–500ms, meaning 10,000 emails would take 30–80 minutes sequentially. Async I/O with aiohttp cuts that to under 5 minutes.

Async Bulk Verification with Rate Limiting

import asyncio import aiohttp from dataclasses import dataclass from typing import List

@dataclass class VerificationResult: email: str status: str score: float

class BulkVerifier: def init(self, api_key: str, max_concurrent: int = 20): self.api_key = api_key self.semaphore = asyncio.Semaphore(max_concurrent) self.base_url = "https://mailvalid.io/api/v1"

async def verify_one(
    self, session: aiohttp.ClientSession, email: str
) -> VerificationResult:
    async with self.semaphore:
        try:
            async with session.post(
                f"{self.base_url}/verify",
                headers={"X-API-Key": self.api_key},
                json={"email": email},
                timeout=aiohttp.ClientTimeout(total=15)
            ) as resp:
                data = await resp.json()
                return VerificationResult(
                    email=email,
                    status=data.get("status", "unknown"),
                    score=data.get("score", 0)
                )
        except Exception:
            return VerificationResult(email=email, status="unknown", score=0)

async def verify_batch(self, emails: List[str]) -> List[VerificationResult]:
    connector = aiohttp.TCPConnector(limit=50)
    async with aiohttp.ClientSession(connector=connector) as session:
        tasks = [self.verify_one(session, email) for email in emails]
        return await asyncio.gather(*tasks)

Usage

async def main(): verifier = BulkVerifier(api_key="mv_live_your_key_here") emails = ["[email protected]", "[email protected]", "[email protected]"] results = await verifier.verify_batch(emails)

for r in results:
    print(f"{r.email}: {r.status} (score: {r.score})")

# Filter to only valid emails
valid = [r for r in results if r.status == "valid"]
print(f"\n{len(valid)}/{len(results)} emails are valid")

asyncio.run(main())

[email protected]: valid (score: 0.97)

[email protected]: invalid (score: 0.03)

[email protected]: risky (score: 0.55)

[!TIP] The max_concurrent=20 semaphore matches MailValid's rate limit of 60 requests per minute for standard API keys. Adjust based on your plan tier. Higher concurrency without rate limiting will trigger 429 responses.

Integrating with Pandas for Data Analysis

If you are cleaning a CSV or a database export, combine bulk verification with Pandas for instant segmentation:

import pandas as pd import asyncio

async def verify_dataframe(df: pd.DataFrame, email_col: str, api_key: str) -> pd.DataFrame: verifier = BulkVerifier(api_key=api_key) emails = df[email_col].tolist() results = await verifier.verify_batch(emails)

results_df = pd.DataFrame([
    {"email": r.email, "status": r.status, "score": r.score}
    for r in results
])

return df.merge(results_df, left_on=email_col, right_on="email", how="left")

Usage

df = pd.read_csv("leads_export.csv") df_verified = asyncio.run(verify_dataframe(df, "email", "mv_live_your_key_here"))

Segment by status

valid_leads = df_verified[df_verified["status"] == "valid"] invalid_leads = df_verified[df_verified["status"] == "invalid"] risky_leads = df_verified[df_verified["status"] == "risky"]

print(f"Valid: {len(valid_leads)} | Invalid: {len(invalid_leads)} | Risky: {len(risky_leads)}")

Valid: 8,432 | Invalid: 1,204 | Risky: 364


Adding Email Verification to Django and Flask

Most Python web applications need email verification at two touchpoints: signup forms and contact/lead imports. Here is how to wire it into the most popular frameworks.

Django: Verify on Signup

# views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt import requests

MAILVALID_API_KEY = "mv_live_your_key_here"

@csrf_exempt def signup_view(request): if request.method != "POST": return JsonResponse({"error": "POST required"}, status=405)

email = request.POST.get("email", "").strip().lower()

# Step 1: Verify email with MailValid
verify_resp = requests.post(
    "https://mailvalid.io/api/v1/verify",
    headers={"X-API-Key": MAILVALID_API_KEY},
    json={"email": email},
    timeout=10
)
result = verify_resp.json()

# Step 2: Reject invalid emails
if result["status"] == "invalid":
    suggestion = result.get("suggestion")
    msg = "Invalid email address."
    if suggestion:
        msg += f' Did you mean {suggestion}?'
    return JsonResponse({"error": msg}, status=400)

# Step 3: Warn about risky emails (catch-all, role-based)
if result["status"] == "risky":
    return JsonResponse({
        "warning": "This email may not receive messages reliably.",
        "checks": result.get("checks", {}),
        "proceed": True
    })

# Step 4: Create user (valid email confirmed)
# user = User.objects.create_user(email=email, ...)
return JsonResponse({"success": True, "email": email})</code_block>

Flask: Verification Middleware

# app.py from flask import Flask, request, jsonify import requests

app = Flask(name) MAILVALID_API_KEY = "mv_live_your_key_here"

def verify_email(email: str) -> dict: resp = requests.post( "https://mailvalid.io/api/v1/verify", headers={"X-API-Key": MAILVALID_API_KEY}, json={"email": email}, timeout=10 ) return resp.json()

@app.route("/api/register", methods=["POST"]) def register(): data = request.get_json() email = data.get("email", "").strip().lower()

if not email:
    return jsonify({"error": "Email required"}), 400

result = verify_email(email)

if result["status"] == "invalid":
    return jsonify({
        "error": "Invalid email address",
        "suggestion": result.get("suggestion")
    }), 400

if result.get("checks", {}).get("disposable"):
    return jsonify({
        "error": "Disposable email addresses are not allowed"
    }), 400

# Proceed with registration
return jsonify({"verified": True, "score": result["score"]})</code_block>

FastAPI Integration with Async Verification

FastAPI is the natural fit for email verification because its async-native architecture lets you verify emails without blocking the event loop. Here is a complete integration pattern:

from fastapi import FastAPI, HTTPException from pydantic import BaseModel, EmailStr import aiohttp

app = FastAPI() MAILVALID_API_KEY = "mv_live_your_key_here"

class SignupRequest(BaseModel): email: EmailStr name: str

class VerificationResponse(BaseModel): email: str status: str score: float disposable: bool

async def verify_email(email: str) -> dict: async with aiohttp.ClientSession() as session: async with session.post( "https://mailvalid.io/api/v1/verify", headers={"X-API-Key": MAILVALID_API_KEY}, json={"email": email} ) as resp: return await resp.json()

@app.post("/api/signup", response_model=VerificationResponse) async def signup(data: SignupRequest): result = await verify_email(data.email)

if result["status"] == "invalid":
    raise HTTPException(
        status_code=400,
        detail=f"Invalid email: {result.get('suggestion', 'check the address')}"
    )

if result.get("checks", {}).get("disposable"):
    raise HTTPException(status_code=400, detail="Disposable emails not allowed")

# Create user in database
# await db.users.insert_one({"email": data.email, "name": data.name})

return VerificationResponse(
    email=result["email"],
    status=result["status"],
    score=result["score"],
    disposable=result.get("checks", {}).get("disposable", False)
)</code_block>

Common Mistakes Python Developers Make with Email Verification

Mistake 1: Verifying After Storing

The most common architectural error is storing the email first and verifying later. This means your database already contains invalid data before you can filter it. Always verify before write. If you need to handle race conditions, use a temporary holding table and promote verified emails to the main user table.

Mistake 2: Ignoring the risky Status

The risky status covers catch-all domains and role-based addresses. These are technically deliverable but have low engagement rates. According to Return Path research, role-based addresses like info@ and admin@ have 40% lower open rates than personal addresses. Do not treat risky the same as valid — segment them separately in your CRM. We cover the engagement impact of role-based addresses in detail in our analysis of how role-based emails destroy campaign metrics.

Mistake 3: Not Handling API Failures

If the MailValid API times out or returns a 500 error, your signup flow should not crash. Use the fallback pattern: on API failure, mark the email as unknown and queue it for re-verification. Never let an API dependency block your core user flow. The retry pattern shown in the MailValidClient class above handles this automatically.

Mistake 4: Caching Results Indefinitely

Email validity changes over time. An address valid today might be invalid in six months. Per RFC 5321 and standard SMTP behavior, mailboxes can be deactivated or reconfigured at any time. Cache verification results for 30 days maximum, then re-verify before sending critical campaigns. The MailValid API returns a cached flag indicating whether the result was freshly checked or served from cache.


Using Webhooks for Async Verification Workflows

For large lists or time-sensitive operations, the synchronous API can create bottlenecks. MailValid's webhook system lets you submit a batch of emails and receive results asynchronously as each verification completes. For a complete walkthrough of building webhook-driven verification pipelines, see our guide to real-time email verification with webhooks.

import requests import hmac import hashlib from flask import Flask, request, jsonify

app = Flask(name) WEBHOOK_SECRET = "your_webhook_secret"

Submit a batch for async verification

def submit_batch(emails: list, webhook_url: str): resp = requests.post( "https://mailvalid.io/api/v1/bulk/verify", headers={"X-API-Key": "mv_live_your_key_here"}, json={ "emails": emails, "webhook_url": webhook_url, "webhook_secret": WEBHOOK_SECRET } ) return resp.json()

Receive webhook callbacks

@app.route("/webhooks/mailvalid", methods=["POST"]) def mailvalid_webhook(): # Verify HMAC signature signature = request.headers.get("X-Webhook-Signature", "") payload = request.get_data() expected = hmac.new( WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest()

if not hmac.compare_digest(signature, expected):
    return jsonify({"error": "Invalid signature"}), 401

data = request.get_json()
email = data["email"]
status = data["status"]

# Process the result
print(f"Received verification: {email} → {status}")
# Update your database, trigger workflows, etc.

return jsonify({"received": True}), 200</code_block>

Webhooks follow RFC 7208 (SPF) and RFC 6376 (DKIM) authentication patterns, with HMAC-SHA256 signatures to prevent spoofing. Each delivery includes exponential backoff retry (1, 2, 4, 8, 16 minutes) with a maximum of 5 attempts.


Testing Your Integration: Local Development and CI/CD

Before deploying email verification to production, test it locally with a simple Flask or FastAPI server. The MailValid API returns consistent response formats regardless of the input, so you can write deterministic tests against real API responses.

Unit Testing with Mock Responses

import unittest from unittest.mock import patch, MagicMock

class TestEmailVerification(unittest.TestCase):

@patch("requests.post")
def test_valid_email_returns_status_valid(self, mock_post):
    mock_post.return_value = MagicMock(
        status_code=200,
        json=lambda: {
            "email": "[email protected]",
            "status": "valid",
            "score": 0.95,
            "checks": {
                "syntax_valid": True,
                "domain_exists": True,
                "mx_found": True,
                "smtp_reachable": True,
                "disposable": False,
                "role_based": False,
                "catch_all": False
            }
        }
    )

    from your_app import verify_email  # Your integration function
    result = verify_email("[email protected]")

    self.assertEqual(result["status"], "valid")
    self.assertGreater(result["score"], 0.7)
    self.assertTrue(result["checks"]["smtp_reachable"])

@patch("requests.post")
def test_disposable_email_flagged(self, mock_post):
    mock_post.return_value = MagicMock(
        status_code=200,
        json=lambda: {
            "email": "[email protected]",
            "status": "invalid",
            "score": 0.08,
            "checks": {
                "syntax_valid": True,
                "domain_exists": True,
                "mx_found": True,
                "smtp_reachable": False,
                "disposable": True,
                "role_based": False,
                "catch_all": False
            }
        }
    )

    from your_app import verify_email
    result = verify_email("[email protected]")

    self.assertTrue(result["checks"]["disposable"])
    self.assertEqual(result["status"], "invalid")

if name == "main": unittest.main()

For integration testing against the live API, use the MailValid sandbox endpoint. The sandbox accepts any API key with the mv_test_ prefix and returns deterministic results based on email patterns — addresses ending in @valid.test return valid, @invalid.test return invalid, and everything else returns risky.

Adding Verification to Your CI/CD Pipeline

If you are deploying a Python application with continuous integration, you can add email verification tests to your pipeline. This catches integration issues before they reach production. For a complete guide on integrating verification into deployment pipelines, see our CI/CD email verification quality gates guide.

[!TIP] Add MAILVALID_API_KEY to your CI environment variables (GitHub Actions secrets, GitLab CI/CD variables, etc.). Never hardcode API keys in your test files or commit them to version control.


Conclusion: Ship Clean Data or Pay the Price

Email verification is not a feature you add later — it is a cost optimization that pays for itself on day one. Every invalid email you send to costs money in ESP fees, damages your sender reputation, and reduces the deliverability of emails to your actual customers. The MailValid API makes verification a single function call in Python, whether you are building a Django signup form, a FastAPI microservice, or a Pandas data pipeline.

[!TIP] Start with 100 free credits at mailvalid.io. No credit card required. The mv_live_ API key works immediately with all code examples in this guide. For high-volume bulk verification, check our pricing — we are 6x cheaper than ZeroBounce.

M

MailValid Team

Email verification experts

Share:

Join developers who verify smarter

Stop letting bad emails hurt your deliverability

100 free credits. $0.001/email after. Credits never expire. No credit card required.

More from MailValid

Verify 100 emails free Start Free