Metadata-Version: 2.4
Name: bouncezero
Version: 0.1.0
Summary: Official Python client for the BounceZero email verification API
Author-email: BounceZero Ltd <support@bouncezero.io>
License: MIT
Project-URL: Homepage, https://bouncezero.io
Project-URL: Documentation, https://docs.bouncezero.io
Project-URL: Source, https://github.com/BounceZero/bouncezero-python
Keywords: email,verification,validation,bouncezero,deliverability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<div align="center">

# BounceZero — Python

### Real-time & bulk email verification with a 5-stage intelligence pipeline

[![PyPI version](https://img.shields.io/pypi/v/bouncezero?color=ec4899&label=pypi)](https://pypi.org/project/bouncezero/)
[![Python versions](https://img.shields.io/pypi/pyversions/bouncezero?color=3776ab)](https://pypi.org/project/bouncezero/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Docs](https://img.shields.io/badge/docs-docs.bouncezero.io-ec4899)](https://docs.bouncezero.io)
[![Status](https://img.shields.io/badge/status-status.bouncezero.io-22c55e)](https://status.bouncezero.io)

[Documentation](https://docs.bouncezero.io) · [Dashboard](https://app.bouncezero.io/dashboard) · [API Status](https://status.bouncezero.io) · [Support](mailto:support@bouncezero.io)

</div>

---

The official Python client for the [BounceZero](https://bouncezero.io) email verification API. A **single file, zero dependencies** (standard library only), and a small, predictable surface designed for production workloads — from a signup form that validates one address in real time to a nightly job that cleans millions.

```python
from bouncezero import BounceZero

client = BounceZero("bz_live_...")
result = client.verify("someone@example.com")

print(result["classification"], result["score"])   # → verified 98
```

## Why BounceZero

Every address is scored across **40+ signals** through a five-stage pipeline — syntax, DNS/MX, SMTP mailbox probing, disposable & role detection, and a Bayesian + ML confidence model — so you catch invalid, risky, and low-value addresses *before* they cost you deliverability.

- ⚡ **Real-time** single verification in ~500 ms–2 s, or **async bulk** for large lists
- 🎯 **Six clear verdicts** (`verified` / `invalid` / `catch_all` / `disposable` / `risky` / `unknown`) plus threat flags
- 🔁 **Idempotent bulk submits** — a network retry can never double-charge you
- 🧪 **Sandbox keys** to build & test without spending credits
- 🔒 **Signed webhooks** with a one-line verifier
- 📦 **Zero dependencies**, typed errors, automatic retries with backoff

## Installation

```bash
pip install bouncezero
```

Requires Python 3.8+. No transitive dependencies.

## Authentication

Grab your API key from the [dashboard](https://app.bouncezero.io/dashboard) and pass it to the client. Keep live keys server-side — never ship them in client code.

```python
from bouncezero import BounceZero

client = BounceZero("bz_live_...")
```

## Quick start

```python
# 1 — Verify a single address
result = client.verify("jane@example.com")
if result["classification"] == "verified":
    add_to_mailing_list(result["email"])

# 2 — Batch up to 100 synchronously
report = client.verify_batch(["a@example.com", "b@example.com"])
print(report["summary"])           # {'verified': 1, 'invalid': 1, ...}

# 3 — Async bulk for large lists (idempotency-safe)
job   = client.verify_bulk(emails, idempotency_key="import-2026-07-11")
final = client.wait_for_bulk(job["job_id"])
csv   = client.bulk_download(job["job_id"])
```

## Verification result

`verify()` returns a dict with the full signal breakdown. The fields you'll use most:

| Field | Type | Description |
|-------|------|-------------|
| `email` | `str` | The address that was checked |
| `classification` | `str` | One of the six verdicts below |
| `score` | `int` | Confidence 0–100 |
| `is_deliverable` | `bool \| None` | `True`/`False`, or `None` when inconclusive |
| `risk_level` | `str` | `low` · `medium` · `high` |
| `recommendation` | `str` | Human-readable guidance |

### Classifications

| Verdict | Meaning | Safe to send? |
|---------|---------|:---:|
| `verified` | Mailbox exists and accepts mail | ✅ |
| `invalid` | Mailbox does not exist — will hard bounce | ❌ |
| `catch_all` | Domain accepts everything; mailbox unconfirmable | ⚠️ |
| `disposable` | Temporary/throwaway provider | ⚠️ |
| `risky` | Concrete negative signals (full mailbox, poor reputation…) | ⚠️ |
| `unknown` | Mail server blocked/greylisted the probe — retry later | ⏳ |

Two rare threat verdicts — `complainer` and `spamtrap` — may also appear; remove those addresses immediately (details in `threat_type`).

## API reference

| Method | Description |
|--------|-------------|
| `verify(email, depth="standard")` | Verify one address. `depth`: `standard` · `deep` · `ultra` |
| `verify_batch(emails)` | Verify up to 100 addresses synchronously |
| `verify_bulk(emails, idempotency_key=None)` | Submit an async bulk job |
| `bulk_status(job_id)` | Poll job progress |
| `bulk_results(job_id, limit=1000, offset=0)` | Paginated results |
| `bulk_download(job_id)` | Download completed results as CSV bytes |
| `wait_for_bulk(job_id, poll_interval=5.0, timeout=3600.0)` | Block until a job finishes |
| `domain(domain)` | Domain-level intelligence (MX, provider, catch-all…) |
| `analyze_list(emails)` | Free list-quality analysis — no verification, no credits |
| `BounceZero.verify_webhook_signature(body, header, secret)` | Validate a webhook signature |

## Bulk verification

Bulk jobs run asynchronously. Submit, then either poll or wait:

```python
job = client.verify_bulk(emails, idempotency_key="crm-sync-42")

# Option A — block until done
final = client.wait_for_bulk(job["job_id"], poll_interval=10)

# Option B — poll yourself
status = client.bulk_status(job["job_id"])
print(status["completed"], "/", status["total"])

# Fetch results (paginated) or download the full CSV
page = client.bulk_results(job["job_id"], limit=1000, offset=0)
csv  = client.bulk_download(job["job_id"])
```

**Idempotency** — pass an `idempotency_key` (any unique string). Replaying the same key returns the *original* job instead of creating and charging for a duplicate, so retries after a timeout are always safe.

## Error handling

Every failure raises a subclass of `BounceZeroError`, each carrying `.status_code` and `.payload`:

```python
from bouncezero import (
    BounceZero, AuthenticationError, InsufficientCreditsError,
    RateLimitError, NotFoundError, BounceZeroError,
)

try:
    result = client.verify("someone@example.com")
except InsufficientCreditsError:
    top_up_credits()
except RateLimitError as e:
    time.sleep(e.retry_after or 60)
except AuthenticationError:
    alert("Check your API key")
except BounceZeroError as e:
    log.error("BounceZero %s: %s", e.status_code, e)
```

| Exception | HTTP | When |
|-----------|:----:|------|
| `AuthenticationError` | 401 / 403 | Missing, invalid, disabled, or IP-restricted key |
| `InsufficientCreditsError` | 402 | Not enough credits |
| `NotFoundError` | 404 | Unknown job or resource |
| `RateLimitError` | 429 | Plan rate limit exceeded (see `.retry_after`) |
| `BounceZeroError` | other | Any other API/transport error |

Requests automatically retry on `429` and `5xx` with exponential backoff (honoring `Retry-After`); tune with `max_retries`.

## Rate limits

Limits are per API key, per minute, by plan. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`.

| Plan | Requests / min |
|------|:---:|
| Free | 30 |
| Starter · Growth | 100 |
| Professional · Business · Scale | 300 |
| Ultimate · Enterprise | 1000 |

## Sandbox / testing

Create a **sandbox key** (`bz_test_` prefix) in the dashboard. It returns deterministic mock results, never spends credits, and never performs a real probe — the local part of the address picks the verdict:

```python
sandbox = BounceZero("bz_test_...")
sandbox.verify("verified@example.com")["classification"]    # → "verified"
sandbox.verify("risky@example.com")["classification"]       # → "risky"
```

Supported on `verify()`. See the [sandbox docs](https://docs.bouncezero.io#sandbox).

## Webhooks

BounceZero signs every webhook with `X-BounceZero-Signature` (HMAC-SHA256). Always verify against the **raw** request body before parsing:

```python
from bouncezero import BounceZero

@app.post("/webhooks/bouncezero")
def handle(request):
    if not BounceZero.verify_webhook_signature(
        request.raw_body,
        request.headers.get("X-BounceZero-Signature", ""),
        signing_secret,        # from Dashboard → Webhooks
    ):
        return 400
    event = request.json()
    ...
```

## Configuration

```python
client = BounceZero(
    api_key="bz_live_...",
    base_url="https://app.bouncezero.io",  # override for a dedicated region
    timeout=60,                            # seconds
    max_retries=2,                         # retries on 429 / 5xx
)
```

## Support

- 📖 Documentation — https://docs.bouncezero.io
- 📊 API status — https://status.bouncezero.io
- ✉️ support@bouncezero.io

## License

[MIT](LICENSE) © BounceZero Ltd
