Email Verification API: Python Example

Published 2026-08-19 · Reviewed by Engineering / Technical Team
Quick answer

Send a POST request to /v1/email/check with an Authorization: Bearer header carrying your API key and a JSON body containing the email address, using Python's requests library. The response includes a decision (allow/review/block), a risk score with confidence, and the specific signals behind it. Below is a complete, working example in Python, plus the equivalent cURL and error-handling pattern.

A basic request

check_email.py
import os
import requests

API_KEY = os.environ["EMAIL_RISK_API_KEY"]

def check_email(email: str, mode: str = "standard", context: str = "signup") -> dict:
    response = requests.post(
        "https://api.emailriskradar.com/v1/email/check",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"email": email, "mode": mode, "context": context},
        timeout=10,
    )
    if not response.ok:
        error = response.json().get("error", {})
        raise RuntimeError(f"Email check failed: {error.get('code')} - {error.get('message')}")
    return response.json()

result = check_email("user@example.com")
print(result["decision"], result["risk"]["score"], result["risk"]["level"])

Both mode and context are optional and default to "standard" and "generic" respectively — set context explicitly whenever the form has a specific purpose (see How to Stop Fake SaaS Signups for why the signup context specifically matters).

The same request in cURL

request.sh
curl https://api.emailriskradar.com/v1/email/check \
  -H "Authorization: Bearer $EMAIL_RISK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "mode": "standard", "context": "signup"}'

Reading the response

response.json
{
  "email": "user@example.com",
  "decision": "allow",
  "risk": { "score": 5, "level": "low", "confidence": 0.8 },
  "verification": { "status": "likely_valid", "domain": "verified", "mailbox": "unconfirmed", "confidence": 0.75 },
  "deliverability": { "status": "likely_deliverable", "confidence": 0.75 },
  "email_quality": { "score": 95, "classification": "normal" },
  "signals": [],
  "checks": {
    "syntax_valid": true,
    "domain_exists": true,
    "mx_found": true,
    "smtp_check": "unknown",
    "disposable": false,
    "free_provider": false,
    "role_based": false,
    "catch_all": false,
    "typo_detected": false
  },
  "meta": { "processing_ms": 84, "cached": false, "mode": "standard", "context": "signup" }
}

In practice, most application code only needs to branch on result["decision"], and read result["risk"] and result["signals"] when you want to log or display why a decision was made:

handle_decision.py
result = check_email(email)

if result["decision"] == "allow":
    create_account(email)
elif result["decision"] == "review":
    create_account(email, requires_confirmation=True)
    send_confirmation_email(email)
elif result["decision"] == "block":
    raise ValueError("This email address can't be used to sign up.")

Handling errors

Errors follow a consistent shape — { "error": { "code", "message", "request_id" } } — with an HTTP status matching the error code, so you can branch on either:

error.codeHTTP statusTypical cause
INVALID_REQUEST400Missing or malformed email field
AUTHENTICATION_REQUIRED / INVALID_API_KEY401Missing, malformed, or revoked API key
RATE_LIMIT_EXCEEDED429Too many requests per second for your plan — back off and retry
QUOTA_EXCEEDED402Monthly check quota exhausted
SERVICE_UNAVAILABLE503A dependency (e.g. Stripe, for billing routes) isn't configured — not expected on /email/check
error_handling.py
import time

def check_email_with_retry(email: str) -> dict | None:
    try:
        return check_email(email)
    except RuntimeError as err:
        code = str(err).split(" - ")[0].split(": ")[-1]
        if code == "RATE_LIMIT_EXCEEDED":
            time.sleep(1)  # simple retry - use exponential backoff in production
            return check_email(email)
        if code == "QUOTA_EXCEEDED":
            # fail open or closed depending on how critical the check is to your flow
            print("Email risk quota exhausted")
            return None
        raise

API keys and environments

Keys are issued per environment (live or test) from the dashboard, and never appear anywhere in a response — store the key in an environment variable, never hard-code it, and never expose it to client-side code. Every request must be made server-side, since the key is a bearer credential with no additional per-request scoping.

Checking many addresses at once

For batches rather than one-at-a-time checks, POST /v1/email/bulk accepts an array of addresses and processes them asynchronously — poll GET /v1/email/bulk/:jobId for status, or register a webhook endpoint to be notified when the batch completes, instead of holding a connection open for a large batch.

FAQ

Does the API have an official Python SDK?

The API itself is just JSON over HTTP, so the requests library (or httpx, urllib3, etc.) works directly with the same request shape shown here — no separate SDK is required.

What's the difference between mode: "fast", "standard", and "full"?

Fast only consults cached DNS/domain data for lowest latency. Standard performs live DNS lookups. Full adds a live SMTP handshake to the mail server, which is slower but gives the strongest deliverability signal. See How SMTP Email Verification Works for what full mode adds specifically.

Can I call this API from a Django or Flask view directly?

Yes — call it server-side from your view or a background task the same way as any other outbound HTTP request. Avoid calling it directly from client-side/browser code, since the API key is a bearer credential meant for server-side use only.

Is there a rate limit?

Yes, per-second rate limits and monthly quotas are set by your plan. A 429 response with RATE_LIMIT_EXCEEDED means back off and retry; a 402 with QUOTA_EXCEEDED means the monthly allowance is used up.

See how this looks against a real address, or start checking your own traffic.

We use Google Analytics to understand site traffic, and only load it if you accept — nothing runs before you choose. Signing in still stores a strictly necessary session token regardless. See the Privacy Policy for details.