Official Python SDK for MailCapture — a real email capture API for integration testing OTP codes, verification links, and other transactional emails.
mailcapture is the Python client for the MailCapture service. Your application sends email to a unique MailCapture address during a test; this library retrieves that email so your test can assert on its contents — subject line, body text, OTP codes, links, and more. Both synchronous and async clients are included.
A MailCapture account is required — free and paid plans are available. Sign up at mailcapture.app.
pip install mailcaptureRequires Python 3.9+.
from mailcapture import MailCapture
with MailCapture(api_key) as mc:
mc.ping() # validates key, caches your username
# Send an email to {username}-signup@mailcapture.app, then:
email = mc.wait_for("signup", timeout=15)
print(email.subject) # "Welcome to Acme!"
print(email.otp) # "123456" — extracted automatically- Clear the inbox before each test
- Trigger the action that sends the email (register, reset password, etc.)
- Wait for the email —
wait_forholds the connection open and returns the instant it arrives - Assert on subject, OTP, body, links
- Clean up after
# pytest example
import pytest
from mailcapture import MailCapture
@pytest.fixture(scope="session")
def mc():
with MailCapture(os.environ["MAILCAPTURE_API_KEY"]) as client:
client.ping()
yield client
def test_signup_otp(mc):
inbox = mc.inbox("signup")
inbox.clear() # clean starting state
register_user(inbox.address) # "alice-signup@mailcapture.app"
email = inbox.wait_for(timeout=10)
assert email.subject == "Verify your email"
assert re.match(r"^\d{6}$", email.otp)import asyncio
from mailcapture import AsyncMailCapture
async def test_signup():
async with AsyncMailCapture(api_key) as mc:
await mc.ping()
inbox = mc.inbox("signup")
await inbox.clear()
await register_user(inbox.address)
email = await inbox.wait_for(timeout=10)
assert email.otp is not NoneBoth clients accept the same constructor arguments.
| Argument | Default | Description |
|---|---|---|
api_key |
required | Your mc_... API key |
base_url |
https://mailcapture.app |
Override for local dev |
request_timeout |
10.0 |
Default timeout in seconds |
Both support context manager usage (with / async with) for clean connection handling.
Validates your API key and returns your address template. Also caches your username so address() works synchronously.
result = mc.ping()
print(result.username) # "alice"
print(result.address_template) # "alice-{tag}@mailcapture.app"Long-polls the API and returns the first email captured for the given tag. The server holds the connection open — no busy-waiting.
email = mc.wait_for("signup", timeout=15)| Argument | Default | Description |
|---|---|---|
tag |
required | Which inbox to watch |
timeout |
30 |
Total wait in seconds |
poll_timeout |
10 |
Per-poll server timeout in seconds (max 30) |
after |
60s ago | Only return captures received after this datetime |
Raises MailCaptureTimeoutError if no email arrives in time.
Returns a scoped inbox object for a tag. Keeps test code clean.
inbox = mc.inbox("password-reset")
inbox.address # "alice-password-reset@mailcapture.app" (requires ping() first)
inbox.wait_for(timeout=10)
inbox.list(limit=5)
inbox.clear()Generates the capture email address synchronously. Requires ping() first.
mc.ping()
mc.address("signup") # "alice-signup@mailcapture.app"List recent captures (newest first).
result = mc.list(tag="signup", limit=10)
for email in result.items:
print(email.subject)Get a single capture by ID. Raises MailCaptureNotFoundError if not found.
Delete all captures for a tag. Use before each test for a clean inbox.
@dataclass
class Capture:
id: str # UUID
tag: str # e.g. "signup"
subject: str # email subject line
otp: str | None # extracted OTP/code, if detected
body_text: str | None
body_html: str | None
latency_ms: int # time from send to capture, in ms
status: str
received_at: str # ISO 8601 timestampThe otp field is extracted automatically. If your OTP is embedded in a sentence, the service finds it for you. None if no code was detected.
All errors extend MailCaptureError and have a .code attribute.
from mailcapture import (
MailCaptureAuthError,
MailCaptureTimeoutError,
MailCaptureNotFoundError,
MailCaptureNetworkError,
)
try:
email = mc.wait_for("signup", timeout=10)
except MailCaptureTimeoutError as e:
print(f"Waited {e.waited_seconds:.0f}s for tag '{e.tag}' — nothing arrived")
print("Did the email actually send? Check your email service logs.")
except MailCaptureAuthError:
print("Check your MAILCAPTURE_API_KEY environment variable.")
except MailCaptureNetworkError:
print("Could not reach MailCapture. Check your network connection.")| Exception | .code |
When |
|---|---|---|
MailCaptureAuthError |
UNAUTHORIZED |
Invalid or revoked API key |
MailCaptureTimeoutError |
TIMEOUT |
wait_for exceeded its timeout |
MailCaptureNotFoundError |
NOT_FOUND |
get(id) — capture not found |
MailCaptureNetworkError |
NETWORK_ERROR |
Could not reach the API |
MailCaptureApiError |
varies | Unexpected API error |
Each tag is its own inbox — safe to run concurrently.
import asyncio
from mailcapture import AsyncMailCapture
async def test_parallel():
async with AsyncMailCapture(api_key) as mc:
await mc.ping()
signup = mc.inbox("signup")
reset = mc.inbox("password-reset")
await asyncio.gather(signup.clear(), reset.clear())
# Trigger both emails...
signup_email, reset_email = await asyncio.gather(
signup.wait_for(timeout=15),
reset.wait_for(timeout=15),
)mc = MailCapture(api_key, base_url="http://localhost:3002")The SDK does not read environment variables automatically. Pass your key explicitly:
import os
mc = MailCapture(os.environ["MAILCAPTURE_API_KEY"])Get your API key at mailcapture.app/admin/api-keys.