zerodrop
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.
- Sign in with GitHub at zerodrop.dev/keys and create a key.
- Set it as
ZERODROP_API_KEY. The client reads it automatically, or you can passapi_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 theZERODROP_API_KEYenvironment 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_— optionalZeroDropFilterinstance
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| zerodrop-0.3.0.tar.gz | 8.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.