Skip to main content

Email you can build on. Send mail, read the mailbox and automate a workspace from code.

Website • Documentation • Every method • API Reference


Intro to the Python Package

The official Python client for the Nextmail API. It has one method for every method of the TypeScript SDK, under the same names in snake_case, typed end to end. There is a synchronous client and an asynchronous one with the same methods, it runs on Python 3.10 and newer, and it depends only on httpx, anyio and typing-extensions.

It carries a workspace API key or an OAuth access token, so it belongs on a server or in a tool that runs on your own machine. The one exception is disposable inboxes, which need no credential.

Installing

pip install nextmail

Or uv add nextmail, or poetry add nextmail.

Using

from nextmail import Nextmail

client = Nextmail('nm_live_...')

sent = client.emails.send(
    {
        'from': 'Acme Billing <billing@acme.com>',
        'to': 'ada@example.com',
        'subject': 'Your September invoice',
        'html': '<p>Your invoice is attached.</p>',
        'attachments': [
            {'filename': 'invoice.pdf', 'content': pdf_bytes, 'contentType': 'application/pdf'}
        ],
    }
)

print(sent['id'], sent['status'])

Request bodies are plain dictionaries with the API's own field names, so 'from', 'scheduledAt' and 'replyTo' read exactly as they do in the API reference. Responses are dictionaries too. Every body and response has a TypedDict in nextmail.types, so your editor completes the keys and a type checker catches a misspelt one.

Create a key in Nextmail under Settings, API keys. It is shown once, and it belongs in an environment variable rather than in code. Nextmail() with no key reads NEXTMAIL_API_KEY:

from nextmail import Nextmail

client = Nextmail()

Build the client once, in a module of its own, and import it everywhere else. It keeps one connection pool, is safe to share between threads, and closes with client.close() or a with block.

Or skip even that. The package ships a ready made nextmail client that reads NEXTMAIL_API_KEY the first time it is touched:

from nextmail import nextmail

nextmail.emails.send({'from': sender, 'to': recipient, 'subject': subject, 'text': text})

Every send carries an idempotency key, generated once per call and reused by its retries, so a retried request replays the original message rather than sending a second one. Pass your own with idempotency_key= to make that hold across processes and restarts.

Async

import asyncio

from nextmail import AsyncNextmail


async def main() -> None:
    async with AsyncNextmail() as client:
        sent = await client.emails.send(
            {'from': sender, 'to': recipient, 'subject': 'Hi', 'text': 'Hello'}
        )

        async for thread in client.threads.iterate(folder='inbox'):
            print(thread['id'])

        print(sent['status'])


asyncio.run(main())

AsyncNextmail has every method Nextmail has, with the same arguments, and runs on asyncio and trio.

Reading the mailbox

page = client.threads.list(folder='inbox', limit=25)

for thread in client.threads.iterate(folder='inbox', query='invoice'):
    full = client.threads.get(thread['id'])

    print(full['messageCount'], full['hasUnread'])

Every paginated resource has list for one page, list_all for every page at once and iterate to stream items and stop whenever you like. A page is {'items': [...], 'hasMore': ..., 'nextCursor': ...}, and list_all returns one list, apart from addresses.list_all, which returns the whole address book.

Errors

from nextmail import NextmailApiError, nextmail

try:
    nextmail.templates.send(
        'order-shipped',
        {
            'from': 'dispatch@acme.com',
            'to': 'ada@example.com',
            'props': {'orderId': 'AC-4192'},
        },
    )
except NextmailApiError as error:
    if error.is_validation:
        print(error.code, error.param, error.request_id)

    raise

An API refusal is one class, NextmailApiError, with status, type, code, param and request_id, plus is_validation, is_not_found, is_rate_limited and friends to branch on. No response at all is NextmailNetworkError, with is_timeout when the deadline passed. Both inherit NextmailError. An argument the client can tell is wrong before anything is sent, such as a malformed key, raises ValueError.

Webhooks

import os

from fastapi import FastAPI, Request, Response
from nextmail import verify_webhook_signature

app = FastAPI()


@app.post('/webhooks/nextmail')
async def webhook(request: Request) -> Response:
    event = verify_webhook_signature(
        payload=await request.body(),
        headers=request.headers,
        secret=os.environ['NEXTMAIL_WEBHOOK_SECRET'],
    )

    print(event['type'], event['data'])

    return Response(status_code=204)

It checks the HMAC in constant time and rejects a delivery more than five minutes old, then returns the parsed event, or raises WebhookVerificationError. Pass the raw body as bytes or text: re-serialising it changes the bytes and the signature will not match. The headers can come from any framework, since the lookup ignores case.

Disposable inboxes

from nextmail import create_temp_mail

temp = create_temp_mail()

inbox = temp.create({'ttlMinutes': 60})

messages = temp.list_messages(inbox['id'], inbox_token=inbox['token'])

create needs no credential and is the only call that returns the inbox token, so keep it.

OAuth access tokens

An app a person connected to Nextmail with OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as access_token:

from nextmail import Nextmail

client = Nextmail(access_token=session.fresh_access_token)

access_token takes the token itself, or a function that returns it. The function runs before every request, so renew the token there when it is close to expiring and the client never has to be rebuilt. On AsyncNextmail the function may also be async. Pass api_key or access_token, not both. Nextmail() reads NEXTMAIL_ACCESS_TOKEN when you pass neither and NEXTMAIL_API_KEY is not set. me.get() answers 'object': 'oauth_token' for a token, with the connected app's clientId and expiresAt, when the person's approval of the app runs out.

A token acts for a person, so before a sensitive change, such as deleting a domain or changing a webhook, it is asked for the same verification code the web app asks for. The request fails with is_step_up_required. Ask for a code, check it, then replay the request:

from nextmail import NextmailApiError

try:
    client.domains.delete(domain_id)
except NextmailApiError as error:
    if not error.is_step_up_required:
        raise

    challenge = client.security.begin_step_up()

    if challenge['method'] == 'email':
        prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: '
    else:
        prompt = 'Enter the code from your authenticator app, or a backup code: '

    client.security.verify_step_up({'code': input(prompt)})
    client.domains.delete(domain_id)

An emailed code works for 10 minutes, and begin_step_up({'resend': True}) sends a fresh one. Once a code is verified the app is not asked again for 60 minutes. security.step_up_status() says whether it is verified right now. API keys are never asked for a code.

Configuring

Pass keyword arguments when the defaults are not right:

import os

import httpx
from nextmail import Nextmail

client = Nextmail(
    os.environ['NEXTMAIL_API_KEY'],
    base_url='https://api.nextmail.sh',
    timeout=30,
    max_retries=2,
    http_client=httpx.Client(proxy='http://proxy.internal:3128', follow_redirects=True),
    headers={'X-Team': 'billing'},
)

The shipped nextmail client takes the same arguments through init(...), called once at startup.

base_url also comes from NEXTMAIL_BASE_URL. Use an https: origin: the client refuses to send an API key, an access token or an inbox token over plain http:, and raises before the request leaves, unless the server is on this machine at localhost, a 127.x.x.x address or ::1. A base_url on 0.0.0.0 raises when the client is built, since that is the address a server listens on: use 127.0.0.1 with the same port.

timeout is in seconds and bounds the whole attempt, the response body included, and 0 turns it off. Reads are retried on 408 and 5xx with backoff. A 429 is retried only when it carries a Retry-After, and any wait longer than a minute raises instead of sleeping. Writes that cannot safely repeat are not retried. Every method outside temp_mail takes api_key= and timeout=, so one process can serve several workspaces with one client. The temp_mail methods take inbox_token= instead of api_key=.

An endpoint no method wraps yet is one client.raw.request() away, with the client's credential, base URL, timeout and retry policy applied:

result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})

The path must begin with a single /. Anything else, such as //host/x, raises before a request is sent, and so does a path whose finished URL leaves the base URL's origin, so the credential it carries never reaches another host.

When a newer version is on PyPI the client says so once on a terminal. NEXTMAIL_DISABLE_UPDATE_NOTICE=1 or disable_update_notice=True turns that off.

Metadata

Release files for nextmail 0.0.1

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

Source distribution (sdist)

Source distribution for nextmail 0.0.1
File Size Uploaded Provenance
nextmail-0.0.1.tar.gz 206.8 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 491.0 kB

Release files / nextmail-0.0.1.tar.gz

Download URL nextmail-0.0.1.tar.gz
Size 206.8 kB
Tags Source
SHA-256 checksum
How to use checksums
a21f67f8804a9576374180c080e1dfa3ca2c18cf5918f3e75df08a4b433eed02
BLAKE2b-256 checksum
How to use checksums
adbb1c5ddda47eb1bdc29b1a81b3a438736eb76e90fb5a8638b2cc829619c6ee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / nextmail-0.0.1-py3-none-any.whl

Download URL nextmail-0.0.1-py3-none-any.whl
Size 284.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3fafe0690ad9ea687116f9799218494cd69bbd39d5fd9b34abcb78414c13441c
BLAKE2b-256 checksum
How to use checksums
0db2dddc83e9e87e2238b5d8f60e9369a34a79a5663e33c0d616c5f52c142a4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for nextmail 0.0.1

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

0.0.1 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