Email Verification API: Python Example
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
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
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
{
"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:
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.code | HTTP status | Typical cause |
|---|---|---|
| INVALID_REQUEST | 400 | Missing or malformed email field |
| AUTHENTICATION_REQUIRED / INVALID_API_KEY | 401 | Missing, malformed, or revoked API key |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests per second for your plan — back off and retry |
| QUOTA_EXCEEDED | 402 | Monthly check quota exhausted |
| SERVICE_UNAVAILABLE | 503 | A dependency (e.g. Stripe, for billing routes) isn't configured — not expected on /email/check |
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
raiseAPI 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
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.
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.
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.
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.