Official Python SDK for the VerifyMail API — disposable, throwaway, and abusive email detection.
Project description
verifymailapi
Official Python SDK for the VerifyMail API — detect disposable, throwaway, and abusive emails before they reach your database.
from verifymailapi import VerifyMail
vm = VerifyMail(api_key="dc_...")
r = vm.check("user@example.com")
print(r.verdict.recommendation) # "allow" | "allow_with_flag" | "block"
Install
pip install verifymailapi
# or
uv add verifymailapi
Requires Python 3.10+. One dependency: httpx.
Server-side only. The API key gives full read/write access to your account. Keep it in your backend /
.env/ secrets store, never in a client app.
Production-ready example
import os
from verifymailapi import (
VerifyMail,
QuotaExceededError,
RateLimitError,
VerifyMailError,
)
# Reuse one instance across requests.
vm = VerifyMail(api_key=os.environ["VERIFYMAIL_KEY"], risk_profile="balanced")
def handle_signup(email: str, request_id: str):
try:
r = vm.check(email, idempotency_key=f"signup:{request_id}")
except QuotaExceededError:
# Out of credits — don't bounce real customers. Allow with a flag.
return {"ok": True, "action": "allow_with_flag", "reason": "verifymail_unavailable"}
except RateLimitError as e:
return {"ok": False, "error": "Please retry in a moment.", "retry_after": e.retry_after}
except VerifyMailError as e:
# Log the error and fail open — losing one signup hurts more than
# briefly skipping fraud detection.
print(f"VerifyMail error: {e.code} {e.message} (req {e.request_id})")
return {"ok": True, "action": "allow", "reason": "verifymail_error"}
rec = r.verdict.recommendation
if rec == "block":
return {"ok": False, "error": r.verdict.summary}
if rec == "allow_with_flag":
# Route through your verification step (email confirmation, captcha,
# extra onboarding step — whatever your app already has).
return {"ok": True, "action": "allow_with_flag"}
return {"ok": True, "action": "allow"}
Async
import asyncio
from verifymailapi import AsyncVerifyMail
async def main():
async with AsyncVerifyMail(api_key="dc_...") as vm:
r = await vm.check("user@example.com")
print(r.verdict.recommendation)
asyncio.run(main())
Same method names, same return types — just await them. Use this from FastAPI, aiohttp, or any asyncio codebase.
API
VerifyMail(api_key, *, base_url=None, retries=2, timeout=30.0, risk_profile=None)
| Argument | Default | Notes |
|---|---|---|
api_key |
required | Your dc_… key from the dashboard. |
base_url |
"https://api.verifymailapi.com" |
Override for staging. |
retries |
2 |
Retries on 429 / 5xx. Set 0 to disable. |
timeout |
30.0 |
Per-request timeout in seconds. |
risk_profile |
None (server default) |
"strict" / "balanced" / "permissive". Per-call override available. |
Methods (same on VerifyMail and AsyncVerifyMail)
| Method | Returns | What it does |
|---|---|---|
check(email, *, risk_profile=, idempotency_key=) |
CheckResponse |
Check a single email. 1 credit. |
check_domain(domain, *, ...) |
CheckResponse |
Domain-only check. 1 credit. |
check_bulk(emails, *, ...) |
BulkCheckResponse |
1–100 emails. Charges N up front. |
check_bulk_stream(emails, *, risk_profile=) |
iterator | NDJSON stream — yields rows as each finishes. |
check_async(email, webhook_url, *, webhook_secret=, ...) |
AsyncCheckResponse |
Returns 202 + preliminary verdict. Final result POSTed to your webhook. |
report(domain, outcome, *, notes=) |
ReportResponse |
File a domain-outcome report. |
usage() |
UsageMeResponse |
Current-period totals + credit balance. |
status() |
StatusResponse |
Component health (Redis / Postgres / DNS). |
Every method that costs credits accepts idempotency_key=True (auto-generated UUID) or a fixed string.
Verdicts — what to do with each
if r.verdict.recommendation == "block":
# High confidence: abuse, dead address, or disposable provider.
reject()
elif r.verdict.recommendation == "allow_with_flag":
# Suspicious. Route through your verification step.
user.requires_email_verification = True
elif r.verdict.recommendation == "allow":
# Clean. Proceed.
pass
The most important rule: map allow_with_flag to user.requires_email_verification = True (or whatever your friction step is called). Most B2B apps already have email verification — that one line costs zero new code and catches the vast majority of bot signups.
Errors
from verifymailapi import (
VerifyMailError,
InvalidApiKeyError,
QuotaExceededError,
RateLimitError,
IdempotencyConflictError,
ValidationError,
ServiceDegradedError,
)
try:
vm.check(email)
except QuotaExceededError as e:
show_billing(e.upgrade_url)
except RateLimitError as e:
retry_after(e.retry_after) # seconds
except InvalidApiKeyError:
alert_ops("VerifyMail key rotated?")
except VerifyMailError as e:
log(e.code, e.status, e.request_id, e.message)
Every error carries code, status, request_id, docs_url, and the raw body payload when available. Subclasses add specific fields (RateLimitError.retry_after, QuotaExceededError.upgrade_url, etc.).
Idempotency
POST endpoints that charge credits all accept idempotency_key. Replay the same key within 24 hours and you get the cached response — no duplicate work, no duplicate charge.
# Auto-generate a UUID
vm.check(email, idempotency_key=True)
# Or pass your own (correlate with your request)
vm.check(email, idempotency_key=f"signup:{request_id}")
Reusing the same key with a different request body raises IdempotencyConflictError (HTTP 409).
Bulk processing
Small batches (≤ 100 emails)
result = vm.check_bulk(["a@x.com", "b@x.com", "c@x.com"])
for r in result.items:
print(r.meta.domain, "→", r.verdict.recommendation)
print(f"charged {result.summary.credits_charged} credits "
f"in {result.summary.elapsed_ms}ms")
Large batches (5k–100k addresses)
Stream results as each check completes:
from verifymailapi import BulkStreamSummary
for event in vm.check_bulk_stream(big_list_of_emails):
if isinstance(event, BulkStreamSummary):
print("done — credits remaining:", event.credits_remaining)
else:
process_row(event.index, event.result)
Async version:
async for event in vm.check_bulk_stream(big_list_of_emails):
...
Results arrive in finish order, not input order — correlate via event.index.
Async deep checks (webhooks)
r = vm.check_async(
email="user@example.com",
webhook_url="https://your-app.example/webhooks/verifymail",
webhook_secret=os.environ["VERIFYMAIL_WEBHOOK_SECRET"],
)
print(r.preliminary.verdict.recommendation) # act on this now
# Final verdict is POSTed to webhook_url after the deep SMTP probe.
Verifying the webhook signature (FastAPI)
from fastapi import FastAPI, Request, HTTPException
from verifymailapi import verify_webhook
import os, json
app = FastAPI()
@app.post("/webhooks/verifymail")
async def webhook(request: Request):
raw = await request.body() # must be raw bytes
sig = request.headers.get("X-VerifyMail-Signature", "")
if not verify_webhook(raw, sig, os.environ["VERIFYMAIL_WEBHOOK_SECRET"]):
raise HTTPException(status_code=401, detail="bad signature")
event = json.loads(raw)
# event["result"] is the final CheckResponse JSON.
return {"ok": True}
Same idea in Flask / Django — just keep the body raw until after verify_webhook returns True.
Framework recipes
Django view
from django.http import JsonResponse
from verifymailapi import VerifyMail
vm = VerifyMail(api_key=settings.VERIFYMAIL_KEY)
def signup(request):
email = request.POST["email"]
r = vm.check(email)
if r.verdict.recommendation == "block":
return JsonResponse({"error": r.verdict.summary}, status=422)
User.objects.create(
email=email,
requires_verification=(r.verdict.recommendation == "allow_with_flag"),
)
return JsonResponse({"ok": True})
FastAPI (async)
from fastapi import FastAPI
from verifymailapi import AsyncVerifyMail
import os
vm = AsyncVerifyMail(api_key=os.environ["VERIFYMAIL_KEY"])
app = FastAPI()
@app.post("/signup")
async def signup(email: str):
r = await vm.check(email)
if r.verdict.recommendation == "block":
return {"error": r.verdict.summary}
return {"ok": True, "flagged": r.verdict.recommendation == "allow_with_flag"}
Rate limits
The API enforces 600 requests / minute per key by default (configurable for paying customers). The SDK automatically:
- Reads
Retry-Afteron429responses - Backs off and retries up to
retriestimes - Raises
RateLimitErrorif all retries fail
Configuration via environment
The SDK doesn't read env vars itself — you pass them. Recommended names:
| Var | Purpose |
|---|---|
VERIFYMAIL_KEY |
Your dc_… API key |
VERIFYMAIL_WEBHOOK_SECRET |
Optional shared secret for vm.check_async(...) |
VERIFYMAIL_API_URL |
Optional override of base_url for staging |
Links
- Full API docs: https://verifymailapi.com/docs
- Dashboard / API keys: https://verifymailapi.com/dashboard/keys
- Issues / discussions: https://github.com/jt1402/verifymail-python
- Pricing: https://verifymailapi.com/pricing
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file verifymailapi-0.1.0.tar.gz.
File metadata
- Download URL: verifymailapi-0.1.0.tar.gz
- Upload date:
- Size: 13.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
541a279a0c6f0341b38d880517c6e05a032cdc4c5b30837a7b9aea3db461fbf8
|
|
| MD5 |
7f469cc089e3d90d512486eb6e4eb9ff
|
|
| BLAKE2b-256 |
991e584ce6821ebab4b627e5bc931362e8ebea8eafa8fed0dc77f52a601a5d37
|
Provenance
The following attestation bundles were made for verifymailapi-0.1.0.tar.gz:
Publisher:
publish.yml on jt1402/verifymail-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
verifymailapi-0.1.0.tar.gz -
Subject digest:
541a279a0c6f0341b38d880517c6e05a032cdc4c5b30837a7b9aea3db461fbf8 - Sigstore transparency entry: 1516899040
- Sigstore integration time:
-
Permalink:
jt1402/verifymail-python@049101abcbb43eaff5630b56688e52b7b86349c0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jt1402
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@049101abcbb43eaff5630b56688e52b7b86349c0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file verifymailapi-0.1.0-py3-none-any.whl.
File metadata
- Download URL: verifymailapi-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7141a2b2d9be71a855cde0bbae86735f218eb6173600541dfd82d849fab0f36
|
|
| MD5 |
aac1bffe62a19a54c826f89361faddc9
|
|
| BLAKE2b-256 |
3302f3f306bc7f6841dc8313ca74c6469e4be393d82bd97e8f7c0cc1a53af5ec
|
Provenance
The following attestation bundles were made for verifymailapi-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on jt1402/verifymail-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
verifymailapi-0.1.0-py3-none-any.whl -
Subject digest:
b7141a2b2d9be71a855cde0bbae86735f218eb6173600541dfd82d849fab0f36 - Sigstore transparency entry: 1516899393
- Sigstore integration time:
-
Permalink:
jt1402/verifymail-python@049101abcbb43eaff5630b56688e52b7b86349c0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jt1402
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@049101abcbb43eaff5630b56688e52b7b86349c0 -
Trigger Event:
push
-
Statement type: