How to Verify Email Addresses in Python with CheckMail

Updated · April 2026

This guide walks through verifying email addresses in Python with the CheckMail API. You'll see single and batch verification, error handling, a reusable retry helper, and a complete working script. The only dependency is requests, and the whole integration fits in under 50 lines.

Building a verifier from scratch in Python is possible - smtplib, dnspython, a disposable domain list - but cloud providers block port 25, IP reputation is a constant battle, and you'll spend more time maintaining blocklists than shipping your product. The API handles all of that.

§01Prerequisites

You need Python 3.8 or newer and the requests library. Sign up for CheckMail and you'll get 100 free credits instantly - no card required. Keys look like cm_live_... for production and cm_test_... for sandboxing.

python --version  # 3.8+
pip install requests
export CHECKMAIL_KEY=cm_live_xxxxxxxxxxxxxxxx

Keep the key out of your source. Read it from the environment, a .env file loaded with python-dotenv, or whatever secret manager your stack uses. Never commit it to git.

§02Your first verification

A single verification is a GET to /v1/verify with the email as a query parameter and a bearer token in the Authorization header:

# verify.py
import os
import requests

KEY = os.environ["CHECKMAIL_KEY"]

response = requests.get(
    "https://api.checkmail.dev/v1/verify",
    params={"email": "john@example.com"},
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=15,
)
response.raise_for_status()

data = response.json()
print(data)

Run python verify.py. You'll get back a JSON object describing the address. One credit is deducted per definitive result; unknown verdicts are free. Always pass a timeout - requests will otherwise hang indefinitely on a misbehaving network.

§03Understanding the response

Here's the full shape of a successful response:

{
  "email": "john@example.com",
  "status": "valid",
  "checks": {
    "syntax": true,
    "mx_found": true,
    "smtp_valid": true,
    "catch_all": false,
    "disposable": false,
    "role_based": false,
    "free_provider": false
  },
  "suggestion": null,
  "mx_host": "aspmx.l.google.com",
  "cached": false,
  "ms": 1243
}

The status field is the one you'll branch on most of the time:

The suggestion field catches typos - enter john@gnail.con and you'll get "john@gmail.com" back. The full schema is in the docs.

§04Batch verification

For verifying lists - newsletter imports, lead cleanup, periodic hygiene jobs - use the batch endpoint. Up to 500 addresses in one request:

def verify_batch(emails):
    response = requests.post(
        "https://api.checkmail.dev/v1/verify/batch",
        json={"emails": emails},
        headers={
            "Authorization": f"Bearer {KEY}",
            "Content-Type": "application/json",
        },
        timeout=60,
    )
    response.raise_for_status()
    return response.json()

payload = verify_batch([
    "alice@acme.com",
    "bob@bouncy.test",
    "carol@example.org",
])

for r in payload["results"]:
    print(f"{r['email']} → {r['status']}")
print(f"Charged: {payload['charged']} credits")

Each result matches the single-verify schema. The charged field reports the real deduction - unknown results are free, so a batch of 500 can legitimately be billed at 487. Chunk your list in the caller: submitting more than 500 in one request returns 400 batch_too_large. A batch takes longer than a single request, so raise the timeout accordingly.

§05Error handling and retries

Split failures into two buckets. Client errors (4xx) are your problem - bad key, malformed request, quota exhausted - and retrying won't help. Server errors (5xx) and rate limits (429) are transient and should be retried with exponential backoff:

import time
from requests import HTTPError

def verify_with_retry(email, max_attempts=4):
    for attempt in range(1, max_attempts + 1):
        response = requests.get(
            "https://api.checkmail.dev/v1/verify",
            params={"email": email},
            headers={"Authorization": f"Bearer {KEY}"},
            timeout=15,
        )

        if response.ok:
            return response.json()

        # Permanent failure - don't retry
        if 400 <= response.status_code < 500 and response.status_code != 429:
            raise HTTPError(f"Client error {response.status_code}: {response.text}")

        # Transient - back off and retry
        delay = min(2 ** (attempt - 1), 10)
        time.sleep(delay)

    raise RuntimeError(f"Gave up after {max_attempts} attempts")

The backoff is 1s, 2s, 4s, 8s - capped at 10. If you prefer, tenacity offers decorator-based retries with jitter baked in, but the hand-rolled version is easy to read and has no extra dependencies. At scale, add jitter (delay + random.uniform(0, 0.5)) so retries don't stampede.

§06Best practices

§07Complete working example

A drop-in script that takes a list of addresses, batches them, retries transient failures, prints a summary, and flags typo suggestions. Save as verify_list.py:

# verify_list.py
import os
import sys
import time
from collections import Counter
import requests

API = "https://api.checkmail.dev/v1"
KEY = os.environ.get("CHECKMAIL_KEY")
BATCH_SIZE = 500

if not KEY:
    print("Set CHECKMAIL_KEY in the environment", file=sys.stderr)
    sys.exit(1)

session = requests.Session()
session.headers.update({
    "Authorization": f"Bearer {KEY}",
    "Content-Type": "application/json",
})

def verify_chunk(emails, attempt=1):
    response = session.post(
        f"{API}/verify/batch",
        json={"emails": emails},
        timeout=60,
    )

    if response.ok:
        return response.json()

    if (response.status_code == 429 or response.status_code >= 500) and attempt < 4:
        time.sleep(2 ** (attempt - 1))
        return verify_chunk(emails, attempt + 1)

    raise RuntimeError(f"{response.status_code}: {response.text}")

def verify_all(emails):
    results = []
    charged = 0
    for i in range(0, len(emails), BATCH_SIZE):
        chunk = emails[i:i + BATCH_SIZE]
        page = verify_chunk(chunk)
        results.extend(page["results"])
        charged += page["charged"]
        print(f"Processed {len(results)} / {len(emails)}")
    return results, charged

if __name__ == "__main__":
    emails = [
        "alice@acme.com",
        "bob@example.org",
        "typo@gnail.con",
        "throwaway@mailinator.com",
    ]

    results, charged = verify_all(emails)
    summary = Counter(r["status"] for r in results)

    for r in results:
        if r.get("suggestion"):
            print(f"{r['email']} → did you mean {r['suggestion']}?")

    print(dict(summary))
    print(f"Credits charged: {charged}")

That's a production-ready verification pipeline. Wire it into a Celery task, an Airflow DAG, or a plain cron job and you're done.

§08Next steps

Grab an API key and start with 100 free credits. $2 per 1,000 after that. Credits never expire. For the full API reference - domain intelligence, account endpoints, auto-topup - see the docs.

Questions? mail@checkmail.dev