Skip to main content

best-tempmail

Disposable email inboxes for automated testing. Create an inbox, wait for mail, pull out the verification code.

Built for signup flows, password resets, and anything else where a test needs to receive a real email.

pip install best-tempmail

Quick start

No API key needed to try it. The free tier is keyless.

from best_tempmail import TempMail

client = TempMail()

inbox = client.create_inbox()
print(inbox.address)   # abc1234@dextde.site

# trigger your signup flow with that address, then:
message = client.wait_for_message(inbox.address, timeout=55)
print(message.subject)

Getting the verification code

The usual reason to receive email in a test is to read a code out of it. Rather than writing a regex for every service's format, ask for the code:

client = TempMail(api_key=os.environ["BTM_API_KEY"])

inbox = client.create_inbox()
sign_up_with_email(inbox.address)

otp = client.wait_for_otp(inbox.address, timeout=55)
enter_code(otp.code)

code is None when nothing scored highly enough to be trusted. That is deliberate: a wrong code fails a test in a way that is hard to trace, so the API returns nothing rather than a guess. Check otp.candidates if you want to see what else was considered.

pytest example

import os
import pytest
from best_tempmail import TempMail

@pytest.fixture
def mail():
    with TempMail(api_key=os.environ["BTM_API_KEY"]) as client:
        yield client

def test_signup_sends_verification_code(mail, browser):
    inbox = mail.create_inbox()

    browser.goto("/signup")
    browser.fill("#email", inbox.address)
    browser.click("#submit")

    otp = mail.wait_for_otp(inbox.address, timeout=55)
    assert otp is not None and otp.code

    browser.fill("#code", otp.code)
    browser.click("#verify")
    assert browser.is_visible("#welcome")

Waiting for mail

wait_for_message holds one connection open until mail arrives, instead of polling in a loop. It returns None on timeout rather than raising, because nothing has gone wrong: the mail just has not arrived yet.

message = client.wait_for_message(
    inbox.address,
    timeout=55,          # seconds, capped at 55 by the server
    since=last_seen_id,  # optional: return the first message that is not this one
)

if message is None:
    ...  # nothing arrived in time

Without since, anything already in the inbox when the call starts is treated as seen, so you only get genuinely new mail.

Reading messages

messages = client.get_messages(inbox.address)          # newest first
full = client.get_message(inbox.address, messages[0].id)

full.subject
full.text
full.html
full.attachments

Note message.from_ rather than from: the latter is a reserved word in Python.

Attachments

Metadata is available on every plan. Downloading the bytes needs Pro.

message = client.get_message(inbox.address, message_id)

for att in message.attachments:
    print(att.filename, att.size, att.downloadable)

    if att.downloadable:
        file = client.download_attachment(inbox.address, message_id, att.index)
        with open(file.filename, "wb") as f:
            f.write(file.content)

Attachments are addressed by index, not by id: ids are regenerated on every read and do not survive a round trip.

Webhooks

Rather than asking for mail, have it pushed to you.

reg = client.register_webhook("https://your-server.com/hooks/mail")
# store reg.secret: it is shown once, and you need it to verify deliveries

Then verify what arrives. Verify before trusting it. A webhook endpoint is a public URL that receives verification codes, and without a signature check anyone who learns the URL can post fabricated mail to it.

from flask import Flask, request
from best_tempmail import parse_webhook

app = Flask(__name__)

@app.route("/hooks/mail", methods=["POST"])
def mail_hook():
    event = parse_webhook(
        payload=request.get_data(),        # raw bytes, not request.json
        signature=request.headers["X-BTM-Signature"],
        timestamp=request.headers["X-BTM-Timestamp"],
        secret=os.environ["BTM_WEBHOOK_SECRET"],
    )
    print(event["address"], event["message"]["subject"])
    return "", 200

The raw body matters: the signature covers the exact bytes that were sent, so re-serialising a parsed object will never match. Use request.get_data() in Flask, request.body in Django, await request.body() in FastAPI.

parse_webhook raises ValueError when verification fails, so an unverified payload cannot be used by accident. Use verify_webhook_signature instead if you would rather handle the failure yourself.

Errors

Errors are typed, because the right reaction differs.

from best_tempmail import (
    RateLimitError, PaymentRequiredError,
    NotFoundError, AuthenticationError, TimeoutError,
)

try:
    otp = client.get_otp(address, message_id)
except RateLimitError as e:
    time.sleep(e.retry_after or 60)        # worth retrying
except PaymentRequiredError as e:
    print(f"Needs a higher plan than {e.plan}")   # retrying will not help
except NotFoundError:
    ...  # inbox or message is gone, or expired

Network errors, timeouts, 429 and 5xx are retried automatically with backoff. Refusals (401, 402, 404) are not: the request was understood, and repeating it only wastes quota.

Rate limits

The most recent response's limits are always available:

client.get_domains()
print(client.rate_limit)
# RateLimit(limit=2000, remaining=1996, reset=1788000000)

Plans

Free Founders / Developer Pro
Requests/hour 150 2,000 5,000
Inbox creation 3/day per IP unlimited unlimited
Inbox lifetime 2 hours 2 hours 24 hours
Polling, wait, WebSocket yes yes yes
Webhooks no yes yes
OTP extraction no yes yes
Attachment downloads no no yes
Concurrent waits 5 5 20
Commercial use no yes yes

The free tier needs no key at all. See pricing.

Configuration

client = TempMail(
    api_key="btm_sk_live_...",   # omit for the free tier
    timeout=30.0,                # per request, seconds
    max_retries=2,               # 0 disables retrying
    headers={},                  # sent with every request
)

The client can be used as a context manager, which closes the underlying HTTP session when done:

with TempMail(api_key=key) as client:
    inbox = client.create_inbox()

API reference

Method Plan
get_domains() any
health() any
create_inbox(username=None, domain=None) any
get_inbox(address) any
delete_inbox(address) any
get_messages(address, limit=100) any
get_message(address, message_id) any
wait_for_message(address, timeout=30, since=None) any
get_otp(address, message_id) paid
wait_for_otp(address, timeout=30) paid
download_attachment(address, message_id, index) Pro
register_webhook(url) paid
get_webhook() paid
delete_webhook() paid

Full API documentation: best-tempmail.com/api OpenAPI spec: api.best-tempmail.com/v1/openapi.json

Requirements

Python 3.8 or later. Depends on requests.

License

MIT

Metadata

Release files for best-tempmail 1.0.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 best-tempmail 1.0.0
File Size Uploaded Provenance
best_tempmail-1.0.0.tar.gz 14.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for best-tempmail 1.0.0
File Interpreter ABI Platform
best_tempmail-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 28.3 kB

Release files / best_tempmail-1.0.0.tar.gz

Download URL best_tempmail-1.0.0.tar.gz
Size 14.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4ecd56e93d1823e0e467e544ffe7c3909307d586760e2dda93520cb71041de48
BLAKE2b-256 checksum
How to use checksums
8a098ce3da518f5bb838b5640e52010cfadead7ea839d7584b24ed20080d6848
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release files / best_tempmail-1.0.0-py3-none-any.whl

Download URL best_tempmail-1.0.0-py3-none-any.whl
Size 14.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5c0c1c71b86cb050c395332b0399c0e765199b4a8c87b83feaeeb845d41783ea
BLAKE2b-256 checksum
How to use checksums
9d381f47d69d1f7899b8a7ab336e5090e314ffaf06452a1ac7003e91f3590fa7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Security for best-tempmail 1.0.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.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

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