Skip to main content

zerodrop

PyPI version Python versions license

Email verification infrastructure for CI pipelines and AI agents.

Send a verification email. Catch it at the edge. Get email.otp and email.magic_link back — auto-extracted, no regex, no Docker, no signup to try.

email = mail.wait_for_latest(inbox)

email.otp        # "123456" — auto-extracted
email.magic_link # "https://..." — no regex needed

Documentation · GitHub · Status

Install

pip install zerodrop

No dependencies. Python 3.8+.

Zero-Auth Mode (Local Development)

from zerodrop import ZeroDrop

mail = ZeroDrop()
inbox = mail.generate_inbox()
# → "swift-x7k29@zerodrop-sandbox.online"

email = mail.wait_for_latest(inbox, timeout=10000)
print(email.subject)     # "Reset your password"
print(email.otp)         # "123456" — auto-extracted, no regex needed
print(email.magic_link)  # "https://..." — auto-extracted verification link

Free API Key (Private Inboxes)

Without a key, anyone who knows an inbox's name can read it, and one sender can reach only 5 different inboxes a day. With a free key, your inboxes start with your own prefix, only your key can read them, and mail to them isn't limited.

  1. Sign in with GitHub at zerodrop.dev/keys and create a key.
  2. Set it as ZERODROP_API_KEY. The client reads it automatically, or you can pass api_key=.
mail = ZeroDrop()                  # reads ZERODROP_API_KEY when it's set
inbox = mail.generate_inbox()
# → "k7x2-swift-x7k29ab@zerodrop-sandbox.online", where k7x2 is your prefix

Reading a k7x2-… inbox without your key raises ZeroDropAuthError with the reason. Without a key, if ZeroDrop held an email because its sender passed the daily limit, the inbox gets a notice instead, and fetch_latest() and wait_for_latest() raise ZeroDropHeldError saying so (0.3.0 or later; older versions return the notice as if it were the email, or time out when a filter skips it). mail.prefix holds your prefix if you build an address yourself. A key holding a character that can't be sent in an HTTP header (a line break, say) makes ZeroDrop() raise ValueError; the message never includes the key.

CI Pipeline Mode (pytest)

import os
import re
from playwright.sync_api import expect
from zerodrop import ZeroDrop

mail = ZeroDrop(api_key=os.environ.get("ZERODROP_API_KEY"))

def test_password_reset(page):
    inbox = mail.generate_inbox()

    page.goto("/forgot-password")
    page.fill('[name="email"]', inbox)
    page.click('[type="submit"]')

    email = mail.wait_for_latest(inbox, timeout=15000)
    assert "Reset your password" in email.subject

    # No regex — magic_link is auto-extracted at the edge
    page.goto(email.magic_link)
    expect(page).to_have_url(re.compile(r".*/dashboard"))

Email Filtering

Filter emails by sender, subject, body, or extracted fields:

from zerodrop import ZeroDrop, ZeroDropFilter

mail = ZeroDrop()

# Only match emails from a specific sender
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
    from_="noreply@yourapp.com"
))

# Only match emails with an OTP
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
    has_otp=True
))

# Combine filters
email = mail.wait_for_latest(inbox, timeout=15000, filter_=ZeroDropFilter(
    from_="noreply@yourapp.com",
    subject="Verify",
    has_magic_link=True
))

All string filters are case-insensitive partial matches.

OTP Auto-Extraction

ZeroDrop extracts OTP codes and magic links at the edge before emails reach your test suite. No regex required.

email = mail.wait_for_latest(inbox)

email.otp        # "123456" — 4-8 digit verification code
email.magic_link # "https://app.com/verify?token=abc" — verification/reset link
email.body       # Full plain-text body (if you need it)

Both fields are None if not detected in the email.

Parallel CI Runs

generate_inbox() runs locally — no network request:

# Safe to run in parallel — generate_inbox() is local
inboxes = [mail.generate_inbox() for _ in range(50)]

Mail to that many inboxes needs a free API key: without one, one sender can reach only 5 different inboxes a day.

API

ZeroDrop(api_key=None, base_url=BASE_URL)

  • api_key — optional. Defaults to the ZERODROP_API_KEY environment variable. Without a key, inboxes are public to anyone who knows the name.
  • base_url — override the API base URL.

mail.generate_inbox() -> str

Returns a ready-to-use email address instantly. No network request. With a key, the name starts with your prefix.

mail.prefix -> Optional[str]

Your key's inbox prefix ("k7x2"), or None without a key.

mail.fetch_latest(inbox, filter_=None) -> Optional[ZeroDropEmail]

Returns the latest email matching the filter, or None if none matches. Raises ZeroDropHeldError if nothing matches and the inbox has a notice that ZeroDrop held an email.

mail.wait_for_latest(inbox, timeout=10000, poll_interval=2.0, filter_=None) -> ZeroDropEmail

Polls until an email arrives. Raises ZeroDropTimeoutError on timeout, and ZeroDropHeldError at once if ZeroDrop held the email.

  • timeout — milliseconds to wait (default: 10000)
  • poll_interval — seconds between polls (default: 2.0)
  • filter_ — optional ZeroDropFilter instance

Types

@dataclass
class ZeroDropEmail:
    id: str
    from_: str
    to: str
    subject: str
    body: str
    raw_body: str
    received_at: datetime
    otp: Optional[str]        # Auto-extracted OTP code (4-8 digits)
    magic_link: Optional[str] # Auto-extracted verification/reset link

@dataclass
class ZeroDropFilter:
    from_: Optional[str] = None        # Partial match on sender address
    subject: Optional[str] = None      # Partial match on subject line
    body: Optional[str] = None         # Partial match on email body
    has_otp: Optional[bool] = None     # Only match emails with an OTP
    has_magic_link: Optional[bool] = None  # Only match emails with a magic link

Error Handling

from zerodrop import ZeroDrop, ZeroDropTimeoutError, ZeroDropHeldError

try:
    email = mail.wait_for_latest(inbox, timeout=10000)
except ZeroDropTimeoutError:
    print("No email received — check your app is sending correctly")
except ZeroDropHeldError as e:
    # Without an API key, the sender passed the daily limit. The message
    # says so; a free key from https://zerodrop.dev/keys lifts it.
    print(e)
Error When
ZeroDropTimeoutError No email arrived within the timeout
ZeroDropHeldError ZeroDrop held the email: without an API key, its sender passed the daily limit (5 different inboxes a day)
ZeroDropAuthError The inbox belongs to an API key, and the request had no key or another one
ZeroDropNetworkError The API couldn't be reached or answered with an error

Free vs Workspace

Free Workspace
Inbox generation ✓ ✓
OTP auto-extraction ✓ ✓
Magic link extraction ✓ ✓
Email filtering ✓ ✓
Email retention 30 min Extended
Custom domains ✗ ✓
API key ✓ free at zerodrop.dev/keys ✓
Daily limit per sender 5 inboxes without a key, none with one None

Get a Workspace at zerodrop.dev

License

MIT

Metadata

Release files for zerodrop 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zerodrop 0.3.0
File Size Uploaded Provenance
zerodrop-0.3.0.tar.gz 8.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for zerodrop 0.3.0
File Interpreter ABI Platform
zerodrop-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 17.5 kB

Release files / zerodrop-0.3.0.tar.gz

Download URL zerodrop-0.3.0.tar.gz
Size 8.2 kB
Tags Source
SHA-256 checksum
How to use checksums
78cef8ee6ebe7aa9129dda7b8b0931358cc84ab7c3f5f89fdef02c2a7f921ef1
BLAKE2b-256 checksum
How to use checksums
079ea24267b25c09b1fd44a867627d35c729a6a719a8e906c23af3acde33d77a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / zerodrop-0.3.0-py3-none-any.whl

Download URL zerodrop-0.3.0-py3-none-any.whl
Size 9.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
961b6a5bbe24faef077473df5242cbf1a3baa36e7bbe2e5fdb22eda8657f4faa
BLAKE2b-256 checksum
How to use checksums
6e56e957fe338fd92bc1c48799ee1cc4010733d9aea0b533017b75f1f4564864
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for zerodrop 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page