Skip to content

jt1402/verifymail-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

verifymailapi

PyPI version python license

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-After on 429 responses
  • Backs off and retries up to retries times
  • Raises RateLimitError if 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

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages