Metadata-Version: 2.4
Name: verifymailapi
Version: 0.1.0
Summary: Official Python SDK for the VerifyMail API — disposable, throwaway, and abusive email detection.
Project-URL: Homepage, https://verifymailapi.com
Project-URL: Documentation, https://verifymailapi.com/docs
Project-URL: Repository, https://github.com/jt1402/verifymail-python
Project-URL: Issues, https://github.com/jt1402/verifymail-python/issues
Author: VerifyMail
License: MIT
License-File: LICENSE
Keywords: disposable,email,fraud,signup,throwaway,validation,verification,verifymail
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.25
Description-Content-Type: text/markdown

# verifymailapi

[![PyPI version](https://img.shields.io/pypi/v/verifymailapi.svg)](https://pypi.org/project/verifymailapi/) [![python](https://img.shields.io/pypi/pyversions/verifymailapi.svg)](https://pypi.org/project/verifymailapi/) [![license](https://img.shields.io/pypi/l/verifymailapi.svg)](./LICENSE)

**Official Python SDK for the [VerifyMail API](https://verifymailapi.com)** — detect disposable, throwaway, and abusive emails before they reach your database.

```python
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

```bash
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

```python
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

```python
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](https://verifymailapi.com/dashboard/keys). |
| `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

```python
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

```python
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.

```python
# 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)

```python
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:

```python
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:

```python
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)

```python
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)

```python
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

```python
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)

```python
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

- **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
