Skip to main content

nowtempmail

Disposable mailboxes and OTP codes for your tests. Verification codes are extracted server-side, so there is no regex to write.

pip install pytest-nowtempmail     # the fixtures (most people want this)
pip install nowtempmail            # the client on its own
def test_signup(page, ntm_inbox):
    page.goto("/signup")
    page.fill("#email", ntm_inbox.address)
    page.click("text=Create account")
    page.fill("#code", ntm_inbox.wait_for_code())  # only this test's mail

Get a key at https://nowtempmail.com/developers and set NTM_API_KEY. There is no config file and no login command — a key on a command line ends up in ps output and shell history.

Zero runtime dependencies. This installs into test environments that already hold pytest, Playwright and usually an HTTP-client pin from some other SDK, so it deliberately brings nothing that could conflict with them.

Server-side only. /v1 rejects any request carrying an Origin header. This is a library for test runners, CI and backends — not for anything in a page.


The fixtures

Fixture Scope What it is
ntm_inbox session One mailbox, shared by every test. Start here.
ntm_fresh_inbox function A mailbox of this test's own. Costs quota — see below.
ntm_client session The NowTempMail client.
ntm_options session Resolved configuration, for overriding in a conftest.

Why ntm_inbox is shared, and why that is safe

POST /v1/mailboxes is the only endpoint that consumes daily quota, the counter increments on creation, and it is never decremented on delete. A per-test mailbox therefore spends the Free plan's 50 mailboxes/day inside a single CI run — the suite goes green locally, then starts failing with quota_exceeded partway through the third run of the afternoon, and the failure has nothing to do with the code that triggered it.

Sharing is safe because every wait is watermarked. An autouse fixture re-stamps a boundary at the start of each test, and wait_for_code() / wait_for_message() default their after to it — so a code triggered by an earlier test is not a candidate.

The watermark is floored to the whole second and set one second behind, because received_at on the wire has one-second granularity and a CI runner's clock is not the API's clock. The consequence is a real limit: two tests that trigger mail within about a second of each other are not isolated from one another. Fix that by disambiguating on content, which is what you want anyway:

code = ntm_inbox.wait_for_code(sender="github", subject="Sign in")

Use ntm_fresh_inbox only where sharing genuinely cannot work — a test asserting on an empty inbox, one that deletes messages out from under a sibling, or one needing a different domain or TTL. A 40-test file using it burns 40 of the Free plan's 50/day, and teardown does not give any of them back.

Configuration

[tool.pytest.ini_options]
ntm_base_url = "https://nowtempmail.com"
ntm_attach_body = false          # read "What reaches your CI report" first
ntm_watermark_skew_ms = 1000
ntm_mailbox_ttl_seconds = 3600
ntm_mailbox_domain = ""          # empty = the rotation's choice

There is deliberately no ntm_api_key option. An API key in pyproject.toml is an API key in git.

What reaches your CI report

When a wait matches, the message is recorded on the test — and on failure it is printed in the output, which is where "the wrong mail matched" actually gets diagnosed.

Bodies and links are withheld by default. A report is zipped into an artifact, downloaded by whoever has repo read access, and pasted into issues; a password-reset link inside one is a live credential with a working expiry. What you get is sender, subject, timestamp, a link count, and the extracted codes.

The codes are included deliberately even though they are secrets: a verification code is single-use, already spent by the time anyone reads the report, and it is the one value you cannot debug an OTP flow without.

Set ntm_attach_body = true to include bodies and links, and treat the whole artifact as a secret if you do.


The client on its own

from nowtempmail import NowTempMail

ntm = NowTempMail()  # reads NTM_API_KEY
inbox = ntm.mailboxes.create()  # returns once it can RECEIVE
sign_up(inbox.address)
code = inbox.wait_for_code(sender="acme", timeout=120)

mailboxes.create() confirms the mailbox is readable before returning. The mail receiver drops inbound mail if its inbox lookup misses, so "created" and "able to receive" are not the same instant, and a handle returned before the second one hands you a race you cannot see.

Creation is never retried. It is the only call that spends daily quota and the API has no idempotency key, so a transport failure raises MailboxMayExistError rather than quietly spending a second mailbox.

Waiting

inbox.wait_for_code(
    sender="github",  # substring, case-insensitive, or a compiled pattern
    subject=re.compile(r"\d{6}"),
    after=some_datetime,  # default: the mailbox's created_at
    ignore_existing=True,  # for resend flows, where the stale code is the bug
    timeout=120,
    cancel=threading.Event(),
)

after defaults to the mailbox's created_at, not to the moment you called. Mail routinely lands before anyone waits for it — a form submits, mail arrives in 300ms, and the test gets around to waiting a second later. Defaulting to call time makes that message invisible and produces a flake nobody can reproduce.

wait_for_code() never fetches a body: codes ride on the message summary because they are extracted at ingest. One request per poll, no message content transferred.

A timeout raises WaitTimeoutError carrying messages_seen — the number that separates "nothing arrived" from "mail arrived but nothing matched".

Errors

Branch on err.code, not on the class. The API may grow a problem code this package has never heard of, and an unrecognised one arrives on the base class carrying that code verbatim rather than being rewritten.

from nowtempmail import NowTempMailError, QuotaExceededError

try:
    inbox = ntm.mailboxes.create()
except QuotaExceededError as err:
    print(err.retry_after)
except NowTempMailError as err:
    print(err.code, err.request_id)  # request_id is what support asks for

No error message, repr() or log line ever contains the API key.

Two class names differ from the JavaScript SDK, because the literal translations shadow builtins that are OSError subclasses:

JavaScript Python
TimeoutError WaitTimeoutError
ConnectionError TransportError

Quota, plainly

Spends daily quota mailboxes.create() — nothing else
Refunded on delete No. Never.
Free plan 50 mailboxes/day, 30 requests/minute
Check what is left ntm.me().usage.mailboxes_remaining_today

Polling me() is free: it runs in peek mode server-side and does not consume the budget it reports on.


MIT licensed.

Metadata

Release files for nowtempmail 0.1.3

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

Source distribution (sdist)

Source distribution for nowtempmail 0.1.3
File Size Uploaded Provenance
nowtempmail-0.1.3.tar.gz 99.7 kB With provenance Details

Built distribution (wheel)

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

Total release size: 145.0 kB

Release files / nowtempmail-0.1.3.tar.gz

Download URL nowtempmail-0.1.3.tar.gz
Size 99.7 kB
Tags Source
SHA-256 checksum
How to use checksums
688f4e85703d1f8368dd36cebeb3e21a89976953f492cf870f8486abfa9c6c41
BLAKE2b-256 checksum
How to use checksums
1ac0c1244a461571796970725ca0b31e1b26163c7f1570a4fbb933b3804c7680
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

Release files / nowtempmail-0.1.3-py3-none-any.whl

Download URL nowtempmail-0.1.3-py3-none-any.whl
Size 45.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4607aef9492046d382a3ef9972e50c652b0ab961424a8a359d89511dd3e27d7f
BLAKE2b-256 checksum
How to use checksums
888fed6b78eb122054302911feb498567e7598cbf987bf1a5d6ad19b96fecc86
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

Security for nowtempmail 0.1.3

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for nowtempmail-0.1.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

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