| title | Python SDK |
|---|---|
| description | ZeroDrop Python SDK reference — email.otp and email.magic_link auto-extracted. No Docker, no regex, no signup. |
Full API reference for zerodrop — the ZeroDrop Python SDK.
pip install zerodropNo dependencies. Python 3.8+. On Python 3.8 to 3.10, use zerodrop 0.3.0 or later: earlier versions raise a ValueError on every email, because they can't parse its timestamp there.
from zerodrop import ZeroDrop
mail = ZeroDrop(api_key=None, base_url="https://zerodrop.dev")| Parameter | Type | Description |
|---|---|---|
api_key |
str (optional) |
Free API key from zerodrop.dev/keys. Defaults to the ZERODROP_API_KEY environment variable. |
base_url |
str (optional) |
Custom base URL for self-hosted instances |
Without an API key, anyone who knows an inbox's name can read it, one sender can reach only 5 different inboxes a day (past that, the inbox gets a notice from notice@zerodrop.dev instead of the email), and each inbox expires 30 minutes after its latest email, on a shared domain. With a free key (zerodrop 0.2.0 or later), generate_inbox() starts names with your key's prefix, only your key can read those inboxes, and mail to them isn't limited. mail.prefix holds the prefix ("k7x2"), or None without a key.
mail.generate_inbox() -> strGenerates a unique inbox address instantly. No network request. With a key, the name starts with your prefix.
inbox = mail.generate_inbox()
# "swift-x7k2m@zerodrop-sandbox.online" without a key
# "k7x2-swift-x7k2m@zerodrop-sandbox.online" with the key zd_k7x2_…mail.wait_for_latest(
inbox: str,
timeout: int = 10000,
poll_interval: float = 2.0,
filter_: Optional[ZeroDropFilter] = None
) -> ZeroDropEmailPolls until an email arrives in the inbox. Raises ZeroDropTimeoutError if no email arrives within the timeout, and ZeroDropHeldError at once if ZeroDrop held the email (see Error types).
| Parameter | Type | Default | Description |
|---|---|---|---|
inbox |
str |
required | Inbox address to poll |
timeout |
int |
10000 |
Milliseconds to wait |
poll_interval |
float |
2.0 |
Seconds between polls |
filter_ |
ZeroDropFilter |
None |
Optional filter to match specific emails |
# Default — 10s timeout
email = mail.wait_for_latest(inbox)
# Custom timeout
email = mail.wait_for_latest(inbox, timeout=30000)
# With filter
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(has_otp=True))mail.fetch_latest(
inbox: str,
filter_: Optional[ZeroDropFilter] = None
) -> Optional[ZeroDropEmail]Fetches the latest email without waiting. Returns None if the inbox is empty or no email matches the filter. Raises ZeroDropHeldError if nothing matches and the inbox holds a notice that ZeroDrop held an email.
email = mail.fetch_latest(inbox)
if email:
print(email.subject)Filter emails by sender, subject, body, or extracted fields. Useful when multiple emails land in the same inbox.
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 a specific subject
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
subject="Verify your email"
))
# Only match emails that contain an OTP
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
has_otp=True
))
# Only match emails that contain a magic link
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
has_magic_link=True
))
# Combine multiple filters
email = mail.wait_for_latest(inbox, timeout=15000, filter_=ZeroDropFilter(
from_="noreply@yourapp.com",
subject="Reset",
has_magic_link=True
))All string filters are case-insensitive partial matches.
@dataclass
class ZeroDropEmail:
id: str # Unique message ID
from_: str # Sender address
to: str # Recipient address
subject: str # Email subject
body: str # Parsed plain-text body
raw_body: str # Full raw MIME message
received_at: datetime # UTC timestamp
otp: Optional[str] # Auto-extracted OTP code (4-8 digits)
magic_link: Optional[str] # Auto-extracted verification/reset link4-8 digit numeric code extracted from the email body. Detected near labels like code, otp, pin, verification. None if not detected.
email = mail.wait_for_latest(inbox)
print(email.otp) # "123456" or NoneVerification or reset URL extracted from the email body. Detected for URLs containing verify, confirm, reset, token, activate, or auth. None if not detected.
print(email.magic_link) # "https://app.com/verify?token=abc" or None@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 linkRaised when no email arrives within the timeout.
from zerodrop import ZeroDrop, ZeroDropTimeoutError
try:
email = mail.wait_for_latest(inbox, timeout=10000)
except ZeroDropTimeoutError:
print("No email received in time")Raised when ZeroDrop held the email. Without an API key, one sender can reach only 5 different inboxes a day. Past that, the email isn't stored, and the inbox gets a notice from notice@zerodrop.dev instead. wait_for_latest() raises at once rather than waiting for the timeout, and the message is the server's reason. A free key from zerodrop.dev/keys lifts the limit for inboxes under your key's prefix.
from zerodrop import ZeroDropHeldError
try:
email = mail.wait_for_latest(inbox)
except ZeroDropHeldError as e:
print(e) # why, and how to get a free keyZeroDropHeldError is new in zerodrop 0.3.0. Older versions treat the notice as an email: without a filter, wait_for_latest returns it (its otp is None), and a filter such as ZeroDropFilter(has_otp=True) skips it, so the wait ends in ZeroDropTimeoutError.
Raised when the API is unreachable. Includes a link to the status page.
from zerodrop import ZeroDropNetworkError
try:
email = mail.fetch_latest(inbox)
except ZeroDropNetworkError as e:
print(f"Network error — check https://zerodrop.instatus.com")Raised on a 401: the inbox belongs to an account's prefix and the request had no key or the wrong one. The message carries the server's reason.
import re
from playwright.sync_api import expect
from zerodrop import ZeroDrop
mail = ZeroDrop()
def test_signup_email_verification(page):
inbox = mail.generate_inbox()
page.goto("/signup")
page.fill('[name="email"]', inbox)
page.fill('[name="password"]', "TestPassword123!")
page.click('[type="submit"]')
# Wait for verification email
email = mail.wait_for_latest(inbox, timeout=15000)
# OTP auto-extracted — no regex needed
assert email.otp is not None
page.fill('[name="otp"]', email.otp)
page.click('[type="submit"]')
expect(page).to_have_url(re.compile(r".*/dashboard"))
def test_magic_link_login(page):
inbox = mail.generate_inbox()
page.goto("/login")
page.fill('[name="email"]', inbox)
page.click('button:has-text("Send magic link")')
email = mail.wait_for_latest(inbox, timeout=15000)
# magic_link auto-extracted — no HTML parsing needed
assert email.magic_link is not None
page.goto(email.magic_link)
expect(page).to_have_url(re.compile(r".*/dashboard"))generate_inbox() runs locally — no network request, no throttling. Without an API key, though, one sender can reach only 5 different inboxes a day; with a free key, inboxes under your prefix aren't counted:
# Safe to run in parallel — generate_inbox() is local
inboxes = [mail.generate_inbox() for _ in range(50)]| Free | Workspace | |
|---|---|---|
| Inbox generation | ✓ | ✓ |
| OTP auto-extraction | ✓ | ✓ |
| Magic link extraction | ✓ | ✓ |
| Email filtering | ✓ | ✓ |
| Email retention | 30 min | Extended |
| Custom domains | ✗ | ✓ |
| API key | ✓ free | ✓ |
Get a Workspace at zerodrop.dev/pricing
The SDK does not send analytics, usage metrics, or environment data to any server. The only network requests made are explicit inbox polls to zerodrop.dev/api/inbox/{name}.
Your CI pipeline is your business.