---
title: "nextmail.emails"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://nextmail.sh/docs/python/reference/emails"
area: "Python"
category: "Reference"
---

# nextmail.emails

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Send mail now or later, in batches or translated, and follow what happened to it.

### `emails.send()`

Send, schedule or translate and send one email

```python
def send(
    body: EmailSend,
    *,
    idempotency_key: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> SentEmailResource
```

Sends one message now, or holds it for later with `scheduledAt` or an undo window with `cancellableForSeconds`. The body comes from exactly one source: `html`, `text` or both, a stored `template`, or an existing `draftId`. Naming none is a 422, and `template` alongside `html`, `text` or `draftId` is refused. An immediate send is dispatched inside the request, so the call usually returns a `sent`, `partial` or `failed` message. A held send comes back `queued` or `scheduled`, so read `status` rather than taking a returned call as delivered mail.

Every call carries an `Idempotency-Key`. The SDK generates one per call and reuses it on that call's retries, and the API claims it against the key's own unique index before anything is dispatched, so a retried network failure replays the original message instead of sending a second one. Pass `idempotency_key=` to extend that across processes and restarts, deriving it from what made the send necessary rather than from a clock. A replay returns the stored message in its current state with `replayed` set to `True`. The same key with a different body is a 422 `idempotency_key_reuse`.

Add `translate` to deliver the message in the recipient's language. The translation runs when the request is accepted, before any record exists, so a scheduled send carries the approved wording and a translation that cannot be produced refuses the whole send: nothing is ever delivered untranslated as a fallback. By default the subject is translated too and your original text is placed below the translation, captioned in the target language. It works with `template`, translating what the template rendered, and is refused alongside `draftId` because a draft goes as it was written.

Scopes: `emails:send`.

**Parameters**

- `body['from']` (`RecipientInput`, required): Sender as a string such as `billing@acme.com` or `Acme Billing <billing@acme.com>`, or as a dict such as `{'email': 'billing@acme.com', 'name': 'Acme Billing'}`. It must be an address the key may send as, otherwise 403 `from_address_forbidden`.
- `body['to']` (`RecipientInput | list[RecipientInput]`, required): One recipient or a list of them, each in any form `from` takes. `to`, `cc` and `bcc` together hold at most 50 addresses, and more is a 422 `too_many_recipients`.
- `body['cc']` (`RecipientInput | list[RecipientInput]`): Copy recipients, counted toward the 50 recipient ceiling.
- `body['bcc']` (`RecipientInput | list[RecipientInput]`): Blind copy recipients, counted toward the 50 recipient ceiling.
- `body['replyTo']` (`RecipientInput`): Written into the `Reply-To` header.
- `body['subject']` (`str`): At most 998 characters. Falls back to the template or draft subject when empty.
- `body['html']` (`str`): HTML body, at most 1,000,000 characters.
- `body['text']` (`str`): Plain text body, at most 1,000,000 characters.
- `body['template']` (`EmailSendTemplate`): A stored template, as a dict with `id`, the template's id or slug, and optionally `version`, `props` and `slots`. Leaving out `version` resolves whatever is published at that moment, so pin it when somebody else owns the copy.
- `body['draftId']` (`str`): Sends an existing draft as written. Cannot be combined with `template` or `translate`.
- `body['threadId']` (`str`): Files the sent message into an existing thread.
- `body['headers']` (`dict[str, str]`): Custom headers as a dict, limited to `X-*`, `List-*`, `Reply-To`, `Precedence`, `Auto-Submitted`, `Importance`, `Priority` and `Feedback-ID`. Anything the server sets itself is a 422 `reserved_header`.
- `body['attachments']` (`list[AttachmentInput]`): At most 20 files. Each entry is either an inline file, a dict with `filename`, `content` as `bytes` or a base64 string, and optionally `contentType` (bytes are encoded for you, and inline files are capped at 10 MB in total once decoded), or a stored file such as `{'fileId': 'file_6bb640f5b99e47deb758f1f5'}`, naming a file already uploaded to the workspace, which is how a file larger than the inline cap is sent. A file at an https address is a dict with `url` and optionally `filename` and `contentType`, fetched once when the message is accepted. Inline and `url` files come to at most 25 MB together.
- `body['attachmentDelivery']` (`AttachmentDeliveryMode`): How the files in `attachments` travel. `mime` carries them inside the message, so files that come to more than 25 MB together are refused. `link` uploads each file and puts a download link in the body in its place, so the message itself stays small. `auto` links when the files together come to more than 25 MB, or to more than 2 MB when the `from` domain has an active files domain, and attaches them otherwise. Left out, the sender's mailbox setting applies, and that defaults to `auto`.
- `body['scheduledAt']` (`datetime | str`): A `datetime`, an ISO 8601 instant or a duration such as `PT1H` or `P2D`. At least one second and at most 365 days out. A naive `datetime` is taken as local time.
- `body['followUpAt']` (`datetime | str`): Asks to be reminded if nobody replies: a `datetime`, an ISO 8601 instant or a duration from now such as `P3D`, later than the time the message goes out and at most 365 days out. If no real reply has arrived by then, the thread returns to the top of the inbox, unread, with a push. `follow_ups.list` lists the reminders and `follow_ups.cancel` cancels one.
- `body['cancellableForSeconds']` (`int`): An undo window from 0 to 900 seconds on an immediate send. Refused alongside `scheduledAt`, which is already cancellable until it goes.
- `body['tracking']` (`TrackingRequest`): Overrides the tracking setting for this send, as a dict with `opens` and `clicks`. A key left out takes the setting of the address it is sent from: its own, else its domain catch-all's when the catch-all caught that address, else off.
- `body['signature']` (`bool`): An `html` body goes out exactly as written, so it carries a signature only when this is `True`, while a `text`-only body carries one unless this is `False`. When it is added it is the default signature of the address it is sent from: the one it keeps for replies when the message answers a thread (`threadId` or an `In-Reply-To` header), and the one for new mail otherwise. An address with no signature of its own takes its domain catch-all's when the catch-all caught that address, else the Nextmail footer unless that address turned the footer off. `signatureId` picks another signature of the address. Template sends and encrypted sends never carry one.
- `body['signatureId']` (`str`): Adds one particular signature of the `from` address in place of its default, by the id `signatures.list` returns. It signs an `html` body too, so `'signature': True` is not needed with it, and it cannot be combined with `'signature': False`. An id that is not a signature of the `from` address is a 422 `invalid_parameter`. When the signature is deleted before a scheduled message goes out, the message carries the default of the address instead.
- `body['tags']` (`dict[str, str]`): Up to 10 tags as a dict, keys of 1 to 64 letters, digits, `_` or `-`, values up to 256 characters. Echoed back on every read.
- `body['stream']` (`SendStream`): The lane the message travels in: `'transactional'`, the default, for mail one person is waiting for, or `'broadcast'` for newsletters and other mail to many people. Each lane has its own queue and its own reputation.
- `body['translate']` (`SendTranslateOptions`): A dict with `to` and, optionally, `from`, `includeOriginal` and `subject`. `to` takes a code, an English name or an endonym. `includeOriginal` and `subject` both default to `True`.
- `idempotency_key` (`str`): Your own key, 1 to 255 characters of letters, digits, `_`, `.`, `:` or `-`. Anything else is a 400 `invalid_idempotency_key`.
- `api_key` (`str`): Sends with this key instead of the client's, for a process sending on behalf of several workspaces.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`SentEmailResource`, an `EmailResource` plus `replayed`. Notable keys are `id` (`msg_` plus 24 hex), `status`, `mode`, `from`, `subject`, `scheduledAt`, `cancellableUntil`, `sentAt`, `lastError`, `tags` and, on a translated send only, `translation`.

**Example**

```python
from nextmail import nextmail

sent = nextmail.emails.send(
    {
        'from': 'Acme Billing <billing@acme.com>',
        'to': 'ada@example.de',
        'subject': 'Your September invoice',
        'html': '<p>The invoice is attached. Tell me if anything on it looks wrong.</p>',
        'translate': {'to': 'de'},
        'tags': {'invoice': 'inv_2026_09_4192'},
    },
    idempotency_key='invoice:inv_2026_09_4192',
)

print(sent['id'], sent['status'], sent['replayed'], sent.get('translation'))
```

**Notes**

- A message sent with `stream` set to `'broadcast'` while that lane is paused for too many bounces or complaints is refused with 409 `stream_paused`. `sending.list_streams` shows why, and app mail on the `'transactional'` lane still goes.
- On the `'broadcast'` lane, a message to one recipient whose `headers` hold no `List-Unsubscribe` gets `List-Unsubscribe` and `List-Unsubscribe-Post` for one-click unsubscribe. Whoever uses them joins the unsubscribe list that `suppressions.list_unsubscribes` reads, and broadcast lane mail is not sent to anybody on that list: the recipient is `suppressed` and `email.suppressed` says why. Transactional mail still reaches them.
- A `from` on a domain that is known to the workspace but cannot sign mail yet is refused up front with 409 `domain_not_sendable` and `param` set to `from`, so nothing is accepted that would only fail at dispatch. `addresses.list` shows the same verdict as `canSend` before you send.
- A test key (`oe_test_`) never delivers. The message is marked `sent` with `transport` set to `test` and every recipient `delivered`, so assert on the response and not on an inbox.
- The idempotency fingerprint covers the template version the send resolved to. Retrying an unpinned template send after somebody publishes a new version is a 422 `idempotency_key_reuse`, not a replay. `scheduledAt` is left out of the fingerprint.
- A spent send allowance is a 429 `send_quota_exceeded` with no `Retry-After`, and it resets on the first of the month. The SDK does not retry a 429 that carries no `Retry-After`, so it raises straight away.
- Translation failures refuse the send: 409 `translation_not_configured` when the workspace has no AI, 422 `translation_too_long` past 30,000 characters, 429 `ai_quota_exceeded` when the workspace has used today's AI actions, and 503 `translation_failed` when the provider did not answer.
- A translated send spends one AI action. `is_retryable` is `True` for every 429, but `ai_quota_exceeded` fails the same way until the allowance resets at midnight UTC, so show it to a person or send without `translate`.
- A `followUpAt` that is not later than the time the message goes out, or is more than 365 days out, refuses the send with a 422 whose `param` is `followUpAt`, coded `invalid_follow_up` or `invalid_parameter`, and nothing is sent. The SDK sends a `datetime` as a UTC ISO 8601 string, as it does for `scheduledAt`.

Also available in: API [`POST /emails`](https://nextmail.sh/docs/api/reference/emails#post-emails); TypeScript [`nextmail.emails.send()`](https://nextmail.sh/docs/sdk/reference/emails#send); Ruby [`emails.send`](https://nextmail.sh/docs/ruby/reference/emails#send); PHP [`emails->send`](https://nextmail.sh/docs/php/reference/emails#send); Go [`Emails.Send`](https://nextmail.sh/docs/go/reference/emails#send); Java [`emails().send`](https://nextmail.sh/docs/java/reference/emails#send); C# [`Emails.SendAsync`](https://nextmail.sh/docs/csharp/reference/emails#send); CLI [`nextmail emails send`](https://nextmail.sh/docs/cli/reference/emails#emails-send).

### `emails.send_batch()`

Send up to 100 independent emails in one request

```python
def send_batch(
    emails: Sequence[EmailSend],
    *,
    idempotency_key: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> BatchResultResource
```

Sends each message in order, as if `emails.send` had been called for it, and reports per item. It is never all or nothing: a bad address on item 7 fails item 7 and the rest still go, because a batch that rolled back would turn your retry into a guess about which messages had already been delivered. The call returns whenever the batch was processed, so check `failed` and each item's `status` rather than waiting for an exception.

The batch shares one `Idempotency-Key`, generated once per call or supplied as `idempotency_key=`, and the server derives a separate key per item from it and the item's position. Retrying the same list replays the items that already went and sends only the ones that did not. Reordering the list between attempts changes which body each position's key is bound to, so an item that moved comes back as an `idempotency_key_reuse` error.

Apart from a key or scope failure or a server fault, the call raises only for a problem with the batch as a whole, a 422 for an empty list, more than 100 messages, or more than 10 messages carrying `translate`. Translation costs several model calls per message and they run one after another, so a larger translated batch would time out partway. Split it, or schedule the messages instead.

Scopes: `emails:send`.

**Parameters**

- `emails` (`Sequence[EmailSend]`, required): A list of 1 to 100 messages, each a dict shaped exactly like the body of `emails.send`, `followUpAt` included, and validated on its own.
- `idempotency_key` (`str`): Your own batch key, 1 to 255 characters of letters, digits, `_`, `.`, `:` or `-`.
- `api_key` (`str`): Sends the batch with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`BatchResultResource`, a dict with `sent`, `failed` and `items`. Each item has its `index` and either `status` set to `ok` with the `email` (a `SentEmailResource`), or `status` set to `error` with an `error` carrying `type`, `code`, `message` and, when a field is to blame, `param`.

**Example**

```python
from nextmail import nextmail

shipped = {'AC-4192': 'ada@example.com', 'AC-4193': 'grace@example.com'}
result = nextmail.emails.send_batch(
    [
        {
            'from': 'dispatch@acme.com',
            'to': address,
            'subject': f'Order {order} has shipped',
            'text': 'It is on its way.',
        }
        for order, address in shipped.items()
    ],
    idempotency_key='shipments:2026-09-15',
)

for item in result['items']:
    if item['status'] == 'error':
        print(item['index'], item['error']['code'], item['error']['message'])

print(result['sent'], result['failed'])
```

**Notes**

- Each entry is authorised on its own, so a `from` on a domain that cannot sign yet fails that entry with `domain_not_sendable` while the rest go.
- Once the send allowance of the workspace runs out partway, every remaining item fails with `send_quota_exceeded` while the earlier ones stay sent. It is counted for that workspace alone, so sends from other workspaces never spend it. A workspace on a paid plan with pay as you go turned on keeps sending past it instead.
- Each item with `translate` spends one AI action. Once that account has used today's AI actions, every remaining item with `translate` fails with `ai_quota_exceeded` while items without it still go. Retrying those items fails the same way until the allowance resets at midnight UTC, unless the account has pay as you go turned on.
- An unexpected server fault aborts the batch with a 500 after the earlier items have gone. The SDK retries it with the same key, which replays those items instead of sending them twice.
- Items are processed one after another inside a single request, so a large batch of immediate sends takes noticeably longer than one `send`. Keep the client's `timeout` generous, or pass a longer `timeout=` to this call.

Also available in: API [`POST /emails/batch`](https://nextmail.sh/docs/api/reference/emails#post-emails-batch); TypeScript [`nextmail.emails.sendBatch()`](https://nextmail.sh/docs/sdk/reference/emails#sendBatch); Ruby [`emails.send_batch`](https://nextmail.sh/docs/ruby/reference/emails#sendBatch); PHP [`emails->sendBatch`](https://nextmail.sh/docs/php/reference/emails#sendBatch); Go [`Emails.SendBatch`](https://nextmail.sh/docs/go/reference/emails#sendBatch); Java [`emails().sendBatch`](https://nextmail.sh/docs/java/reference/emails#sendBatch); C# [`Emails.SendBatchAsync`](https://nextmail.sh/docs/csharp/reference/emails#sendBatch); CLI [`nextmail emails send-batch`](https://nextmail.sh/docs/cli/reference/emails#emails-send-batch).

### `emails.translate()`

Preview a translation without sending anything

```python
def translate(
    body: EmailTranslate,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> TranslationResource
```

Runs the same translation `translate` performs on a send and stops one step early. The same function produces both, so what comes back is what would go out. Nothing is stored and nothing is sent. Use it when somebody should read the translated wording before it reaches a recipient.

Send the approved result as an ordinary `subject` and `html` on `emails.send` with no `translate` in the body. Passing `translate` again translates a second time, moving the wording off the version that was signed off and discarding any edits. When `includeOriginal` is `True`, `html` already contains your original text below the translation, so do not append your own copy.

At least one of `html`, `text` or `subject` is required. `to` accepts a BCP-47 code, an English name or the language's own name, and the response reports the code it settled on as `code` in `language`, which is the form worth storing. State `from` to skip language detection. Otherwise it is detected from the body, and a detector that cannot tell returns `None` in `detectedSourceLanguage` rather than guessing.

Scopes: `emails:send`.

**Parameters**

- `body['to']` (`str`, required): Target language as a code (`de`), English name (`German`) or endonym (`Deutsch`). An unrecognised value is a 422 `invalid_parameter` on `to`.
- `body['from']` (`str`): The language you wrote in. Stating it skips the detection call.
- `body['includeOriginal']` (`bool`): Defaults to `True`, placing your original text below the translation under a caption in the target language.
- `body['subject']` (`str`): Subject line to translate, at most 998 characters.
- `body['html']` (`str`): HTML body to translate. Only the content inside `<body>` is sent to the model when the markup is a full document.
- `body['text']` (`str`): Plain text body to translate. Translated separately when given alongside `html`.
- `api_key` (`str`): Runs the preview with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`TranslationResource`, a dict with `object` set to `translation`, `language` and `detectedSourceLanguage` as full `LanguageResource` dicts, `subject`, `html` and `text` (each `None` when that input was not given) and `includeOriginal`.

**Example**

```python
from nextmail import nextmail

preview = nextmail.emails.translate(
    {'to': 'ja', 'subject': 'Your September invoice', 'html': '<p>The invoice is attached.</p>'}
)

print(preview['language']['native'], preview['subject'])

nextmail.emails.send(
    {
        'from': 'Acme Billing <billing@acme.com>',
        'to': 'ada@example.jp',
        'subject': preview['subject'] or 'Your September invoice',
        'html': preview['html'] or '<p>The invoice is attached.</p>',
    }
)
```

**Notes**

- The SDK never retries this call. Each attempt spends one AI action, as a translated send does. Handle a 503 `translation_failed` yourself, and treat a 429 `ai_quota_exceeded` as final until the allowance resets at midnight UTC.
- Anything over 30,000 characters is refused with 422 `translation_too_long` rather than truncated, since half a translation has no seam to show where it stopped.
- A right-to-left target comes back with `html` wrapped in `dir="rtl"`.
- A workspace with no AI configured gets 409 `translation_not_configured`, and retrying will fail the same way.

Also available in: API [`POST /emails/translate`](https://nextmail.sh/docs/api/reference/emails#post-emails-translate); TypeScript [`nextmail.emails.translate()`](https://nextmail.sh/docs/sdk/reference/emails#translate); Ruby [`emails.translate`](https://nextmail.sh/docs/ruby/reference/emails#translate); PHP [`emails->translate`](https://nextmail.sh/docs/php/reference/emails#translate); Go [`Emails.Translate`](https://nextmail.sh/docs/go/reference/emails#translate); Java [`emails().translate`](https://nextmail.sh/docs/java/reference/emails#translate); C# [`Emails.TranslateAsync`](https://nextmail.sh/docs/csharp/reference/emails#translate); CLI [`nextmail emails translate`](https://nextmail.sh/docs/cli/reference/emails#emails-translate).

### `emails.check()`

Check how a message would be rated, without sending it

```python
def check(
    body: EmailCheck,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailCheckResource
```

Scores a message the way a receiving mailbox would, before it goes out: a spam score, a phishing score and an AI-writing score, each 0 to 100, with the signals behind each one. Nothing is stored and nothing is sent.

The same checks score every message that arrives in a mailbox on Nextmail, so what comes back is what a recipient on Nextmail sees in Details. It cannot know a recipient's own filter, sender history or reputation, so a low score is a good sign and not a delivery guarantee.

Run it before an automated send, or as someone writes, and fix what `signals` names. Sender authentication is taken as passing, since the message will be signed for your domain.

Scopes: `emails:send`.

**Parameters**

- `body['subject']` (`str`): Subject line, at most 998 characters.
- `body['html']` (`str`): HTML body. Links and images are read from it.
- `body['text']` (`str`): Plain text body. Taken from `html` when left out.
- `body['from']` (`str`): The address it will be sent from.
- `body['fromName']` (`str`): The display name it will carry. A name that claims another address or a known brand raises the phishing score.
- `body['replyTo']` (`str`): A Reply-To on a different domain raises the phishing score.
- `body['replying']` (`bool`): `True` when it answers an existing thread. A `Re:` subject on a message that answers nothing raises the spam score.
- `body['attachmentNames']` (`list[str]`): A list of file names, so an attachment that can run code is caught.
- `api_key` (`str`): Runs the check with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailCheckResource`, a dict with `object` set to `email_check` and three scores. `spam` carries `score`, `level` (`low`, `medium` from 35, `high` from 60) and `signals`. `phishing` carries `score`, `level` (`clear`, `caution` from 30, `danger` from 60), `signals` and `reasons`. `ai` carries `score` (`None` when not judged), `level`, `signals`, `reasons`, `words` and `skipped` (`too-short` under 40 words).

**Example**

```python
from nextmail import nextmail

check = nextmail.emails.check(
    {
        'from': 'billing@acme.com',
        'subject': 'Your September invoice',
        'html': '<p>The invoice is attached.</p>',
    }
)

if check['spam']['level'] != 'low':
    print('Rework it first:', check['spam']['signals'])

print(check['spam']['score'], check['phishing']['score'], check['ai']['score'])
```

**Notes**

- It spends no AI action and never calls a model, so it is safe to run on every revision.
- A score is not a probability. Each one adds up weighted signals, heaviest first in `signals`.

Also available in: API [`POST /emails/check`](https://nextmail.sh/docs/api/reference/emails#post-emails-check); TypeScript [`nextmail.emails.check()`](https://nextmail.sh/docs/sdk/reference/emails#check); Ruby [`emails.check`](https://nextmail.sh/docs/ruby/reference/emails#check); PHP [`emails->check`](https://nextmail.sh/docs/php/reference/emails#check); Go [`Emails.Check`](https://nextmail.sh/docs/go/reference/emails#check); Java [`emails().check`](https://nextmail.sh/docs/java/reference/emails#check); C# [`Emails.CheckAsync`](https://nextmail.sh/docs/csharp/reference/emails#check); CLI [`nextmail emails check`](https://nextmail.sh/docs/cli/reference/emails#emails-check).

### `emails.list()`

List one page of sent emails, newest first

```python
def list(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    status: EmailStatus | Sequence[EmailStatus] | None = None,
    from_: str | None = None,
    broadcast_id: str | None = None,
    scheduled_from: datetime | str | None = None,
    scheduled_to: datetime | str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    recipient: str | None = None,
    tag: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[EmailResource]
```

Returns one page of the workspace's send records, ordered newest first. Paging is keyset rather than offset: `nextCursor` is the id of the last message on the page, and passing it back as `cursor=` continues strictly after it, so messages sent while you page never shift or repeat rows. `hasMore` is `False` on the last page and `nextCursor` is then `None`.

Filter with `status=`, one value or a list that the SDK joins with commas, with `from_=`, which matches the sending address exactly and ignores case, with `broadcast_id=`, which keeps the copies of one broadcast, and with `scheduled_from=` and `scheduled_to=`, which keep the messages scheduled inside a window. An unrecognised status is a 422 `invalid_parameter` naming the offending values, and a cursor that names no message in the workspace is a 400 `invalid_cursor`.

Rows are the summary form. They never carry `recipients` or `translation`, whose absence on a row says nothing either way, and a tracked message carries only the counts half of `tracking`: `opens`, `clicks`, `opened`, `clicked`, `openCount`, `clickCount` and `firstOpenAt`. Call `get` for per-recipient delivery state and `get_tracking` for the full engagement report.

Scopes: `emails:read`.

**Parameters**

- `limit` (`int`): Rows per page, a whole number from 1 to 100, defaulting to 25. Outside that range is a 422.
- `cursor` (`str`): The `nextCursor` from the previous page, which is a message id.
- `status` (`EmailStatus | Sequence[EmailStatus]`): One status or a list of them, from `queued`, `scheduled`, `sending`, `sent`, `partial`, `bounced`, `cancelled` and `failed`.
- `from_` (`str`): A bare sending address such as `billing@acme.com`, matched exactly and case insensitively. A display name form does not match.
- `broadcast_id` (`str`): Only the copies of one broadcast, a `brd_` id from `broadcasts.send`. Each person a broadcast reaches gets a message of their own, so this lists who it went to and what happened to each copy. An id that names no broadcast answers an empty page.
- `scheduled_from` (`datetime | str`): Only messages scheduled for this instant or later, as a `datetime` or an ISO 8601 string. With `scheduled_to=` and `status=['scheduled', 'queued']` it lists what is waiting to go out in a window, as the calendar of the app does. A message with no `scheduledAt` is left out.
- `scheduled_to` (`datetime | str`): Only messages scheduled for this instant or earlier, as a `datetime` or an ISO 8601 string. `scheduled_from=` after `scheduled_to=` is a 422 `invalid_parameter`.
- `since` (`datetime | str`): Only messages made at this instant or later, as a `datetime` or an ISO 8601 string.
- `until` (`datetime | str`): Only messages made before this instant. `since=` after `until=` is a 422 `invalid_parameter`.
- `recipient` (`str`): Only messages that went to this address as a to, cc or bcc recipient, matched exactly.
- `tag` (`str`): Only messages sent with this tag: `name` for any value, or `name:value` for one value. Tags are stored encrypted, so the match is exact and case-sensitive.
- `api_key` (`str`): Lists with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[EmailResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `id`, `status`, `mode`, `from`, `subject`, `transport`, `attempts`, `lastError`, `scheduledAt`, `cancellableUntil`, `sentAt`, `tags`, `broadcastId`, `source`, `createdAt` and, when tracked, the trimmed `tracking` counts.

**Example**

```python
from nextmail import nextmail

page = nextmail.emails.list(status=['failed', 'partial'], from_='billing@acme.com', limit=50)

for email in page['items']:
    print(email['id'], email['status'], email['lastError'])

if page['hasMore']:
    following = nextmail.emails.list(
        status=['failed', 'partial'], from_='billing@acme.com', cursor=page['nextCursor']
    )

    print(len(following['items']))
```

**Notes**

- With a narrowed key only messages sent from addresses it covers are read, and the page is cut after that filter, so every page but the last holds `limit` items. A key that holds a whole domain covers every address on it.
- A `from_=` the key does not cover returns an empty last page rather than a 403.
- `tracking` is absent, not zeroed, on a message that carried no pixel or rewritten link.

Also available in: API [`GET /emails`](https://nextmail.sh/docs/api/reference/emails#get-emails); TypeScript [`nextmail.emails.list()`](https://nextmail.sh/docs/sdk/reference/emails#list); Ruby [`emails.list`](https://nextmail.sh/docs/ruby/reference/emails#list); PHP [`emails->list`](https://nextmail.sh/docs/php/reference/emails#list); Go [`Emails.List`](https://nextmail.sh/docs/go/reference/emails#list); Java [`emails().list`](https://nextmail.sh/docs/java/reference/emails#list); C# [`Emails.ListAsync`](https://nextmail.sh/docs/csharp/reference/emails#list); CLI [`nextmail emails list`](https://nextmail.sh/docs/cli/reference/emails#emails-list).

### `emails.list_all()`

Collect every matching sent email into one list

```python
def list_all(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    status: EmailStatus | Sequence[EmailStatus] | None = None,
    from_: str | None = None,
    broadcast_id: str | None = None,
    scheduled_from: datetime | str | None = None,
    scheduled_to: datetime | str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    recipient: str | None = None,
    tag: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[EmailResource]
```

Follows `nextCursor` from page to page and returns once the last page has been read, with every matching send record in one list, newest first. It accepts the same filters as `list` and returns the same summary rows, so there are no `recipients` or `translation` on them and `tracking` is the trimmed counts form.

Everything is held in memory before the call returns, and a workspace's send history grows without bound. Narrow it with `status=` or `from_=`, or switch to `iterate` when you want to stop early or process rows as they arrive. `limit=` sets the page size of each underlying request, not the total, so a larger value means fewer round trips.

Scopes: `emails:read`.

**Parameters**

- `limit` (`int`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`str`): A message id to start after, skipping everything newer.
- `status` (`EmailStatus | Sequence[EmailStatus]`): One status or a list of them to keep, joined with commas on the wire.
- `from_` (`str`): A bare sending address, matched exactly and case insensitively.
- `broadcast_id` (`str`): Only the copies of one broadcast, a `brd_` id.
- `scheduled_from` (`datetime | str`): Only messages scheduled for this instant or later.
- `scheduled_to` (`datetime | str`): Only messages scheduled for this instant or earlier.
- `since` (`datetime | str`): Only messages made at this instant or later.
- `until` (`datetime | str`): Only messages made before this instant.
- `recipient` (`str`): Only messages that went to this address as a to, cc or bcc recipient.
- `tag` (`str`): Only messages sent with this tag: `name` for any value, or `name:value` for one value.
- `api_key` (`str`): Lists with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[EmailResource]` holding every row across all pages.

**Example**

```python
from datetime import datetime, timezone

from nextmail import nextmail

due_today = nextmail.emails.list_all(
    status='scheduled',
    scheduled_from=datetime(2026, 9, 15, tzinfo=timezone.utc),
    scheduled_to=datetime(2026, 9, 15, 23, 59, 59, tzinfo=timezone.utc),
    limit=100,
)

print(len(due_today), [email['id'] for email in due_today])
```

**Notes**

- A failure on any page raises, and the rows already fetched are discarded.
- With a narrowed key only messages sent from addresses it covers are collected, and every page but the last is full.

Also available in: API [`GET /emails`](https://nextmail.sh/docs/api/reference/emails#get-emails); TypeScript [`nextmail.emails.listAll()`](https://nextmail.sh/docs/sdk/reference/emails#listAll); Ruby [`emails.list_all`](https://nextmail.sh/docs/ruby/reference/emails#listAll); PHP [`emails->listAll`](https://nextmail.sh/docs/php/reference/emails#listAll); Go [`Emails.ListAll`](https://nextmail.sh/docs/go/reference/emails#listAll); Java [`emails().listAll`](https://nextmail.sh/docs/java/reference/emails#listAll); C# [`Emails.ListAllAsync`](https://nextmail.sh/docs/csharp/reference/emails#listAll).

### `emails.iterate()`

Stream sent emails one at a time across pages

```python
def iterate(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    status: EmailStatus | Sequence[EmailStatus] | None = None,
    from_: str | None = None,
    broadcast_id: str | None = None,
    scheduled_from: datetime | str | None = None,
    scheduled_to: datetime | str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    recipient: str | None = None,
    tag: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[EmailResource]
```

Returns a generator that yields send records one at a time, newest first, and requests the next page only once the current one is drained. Nothing is fetched until you start consuming it, and breaking out of the loop stops the requests, so this is the cheapest way to find the most recent message matching a condition the filters cannot express.

The walk is keyset based, following `nextCursor` from page to page. Mail sent while you iterate lands ahead of where you started and is never yielded, and nothing already yielded comes round again. Rows are the same summary form `list` returns.

Scopes: `emails:read`.

**Parameters**

- `limit` (`int`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`str`): A message id to start after.
- `status` (`EmailStatus | Sequence[EmailStatus]`): One status or a list of them to keep, joined with commas on the wire.
- `from_` (`str`): A bare sending address, matched exactly and case insensitively.
- `broadcast_id` (`str`): Only the copies of one broadcast, a `brd_` id.
- `scheduled_from` (`datetime | str`): Only messages scheduled for this instant or later.
- `scheduled_to` (`datetime | str`): Only messages scheduled for this instant or earlier.
- `since` (`datetime | str`): Only messages made at this instant or later.
- `until` (`datetime | str`): Only messages made before this instant.
- `recipient` (`str`): Only messages that went to this address as a to, cc or bcc recipient.
- `tag` (`str`): Only messages sent with this tag: `name` for any value, or `name:value` for one value.
- `api_key` (`str`): Lists with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[EmailResource]`, a generator that yields one send record per step.

**Example**

```python
from nextmail import nextmail

for email in nextmail.emails.iterate(status='failed', limit=100):
    if email['tags'].get('invoice') == 'inv_2026_09_4192':
        print(email['id'], email['lastError'])
        break
```

**Notes**

- The generator is lazy, so an abandoned loop costs only the pages you consumed.
- With a narrowed key only messages sent from addresses it covers are yielded, and every page but the last is full.

Also available in: API [`GET /emails`](https://nextmail.sh/docs/api/reference/emails#get-emails); TypeScript [`nextmail.emails.iterate()`](https://nextmail.sh/docs/sdk/reference/emails#iterate); Ruby [`emails.iterate`](https://nextmail.sh/docs/ruby/reference/emails#iterate); PHP [`emails->iterate`](https://nextmail.sh/docs/php/reference/emails#iterate); Go [`Emails.Iterate`](https://nextmail.sh/docs/go/reference/emails#iterate); Java [`emails().iterate`](https://nextmail.sh/docs/java/reference/emails#iterate); C# [`Emails.IterateAsync`](https://nextmail.sh/docs/csharp/reference/emails#iterate).

### `emails.get()`

Read one sent email with per-recipient state

```python
def get(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailResource
```

Returns the full send record for one message: its lifecycle `status`, delivery `attempts` and `lastError`, the schedule fields, and the two parts a list row leaves out. `recipients` has one entry per address with its own `status`, `error` and `deliveredAt`, and `translation` records what was done to a translated send.

A message that went out as a single call can still land differently per recipient. `status` on the message says how the send went as a whole, while each recipient moves on as delivery reports, bounces and complaints arrive. `uncertain` is a real recipient state: a transport that failed partway cannot say which recipients it reached. `suppressed` marks an address that previously bounced or complained in this workspace and was held back.

When the message was tracked, `tracking` carries the full engagement report with per-recipient and per-link detail. When it was not tracked, the key is absent rather than zeroed, because a message with no pixel has no evidence about whether anybody read it. Look it up with `.get('tracking')`.

Scopes: `emails:read`.

**Parameters**

- `id` (`str`, required): The send id, `msg_` followed by 24 hex characters, as returned by `send`.
- `api_key` (`str`): Reads with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailResource` with `id`, `status`, `mode`, `from`, `subject`, `threadId`, `transport`, `attempts`, `lastError`, `scheduledAt`, `cancellableUntil`, `sentAt`, `tags`, `broadcastId`, `source`, `createdAt`, `recipients` and, when present, `translation` and the full `tracking` report.

**Example**

```python
from nextmail import nextmail

email = nextmail.emails.get('msg_3f9a1c07d2b84e6a9c5b1f20')
undelivered = [
    (recipient['email'], recipient['status'], recipient['error'])
    for recipient in email.get('recipients', [])
    if recipient['status'] != 'delivered'
]

print(email['status'], undelivered)
```

**Notes**

- Do not correlate on `messageId`. The header is rewritten on the way out, so that value appears in no bounce or delivery report. Webhook events name the send by this `id`, as `emailId`.
- A 404 never distinguishes a missing id from one in another workspace, and a narrowed key gets the same 404 for a message sent from an address it does not cover.
- `transport` stays `None` until dispatch, and reads `test` on every message sent with a test key.

Also available in: API [`GET /emails/{id}`](https://nextmail.sh/docs/api/reference/emails#get-emails-id); TypeScript [`nextmail.emails.get()`](https://nextmail.sh/docs/sdk/reference/emails#get); Ruby [`emails.get`](https://nextmail.sh/docs/ruby/reference/emails#get); PHP [`emails->get`](https://nextmail.sh/docs/php/reference/emails#get); Go [`Emails.Get`](https://nextmail.sh/docs/go/reference/emails#get); Java [`emails().get`](https://nextmail.sh/docs/java/reference/emails#get); C# [`Emails.GetAsync`](https://nextmail.sh/docs/csharp/reference/emails#get); CLI [`nextmail emails get`](https://nextmail.sh/docs/cli/reference/emails#emails-get).

### `emails.list_events()`

Read one page of the event trail of one sent email

```python
def list_events(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[EmailEventResource]
```

Returns one page of everything recorded against one send, oldest first. Nothing is dropped from the trail, so following `nextCursor` while `hasMore` is `True` reaches its newest event, and `list_all_events` and `iterate_events` do that walk for you. It is the audit behind the current `status`: when the message was accepted or held, when it was rescheduled or cancelled, when it went out or failed, and every delivery report, bounce, complaint, open, click and file download attributed to it afterwards.

Each event has a dotted `type` and a `data` dict whose shape depends on it. `email.accepted`, `email.queued` and `email.scheduled` open the trail with the source and recipient count. `email.sent` names the `transport` and `messageId`. `email.failed` carries the `error`. `email.bounced` and `email.complained` list the affected `recipients` and whether they were suppressed. `email.delivered`, `email.rescheduled`, `email.cancelled`, `email.opened`, `email.clicked` and `email.downloaded` follow as they happen. `email.downloaded` counts a person fetching a file that went out as a download link, never a scanner, and carries no `recipient`: the link is the same for everyone the message went to, so a download cannot be attributed. `data` is an empty dict when an event carries nothing.

Webhooks deliver a subset of these same events as they occur, so this is where to look when a webhook was missed or never subscribed.

Scopes: `emails:read`.

**Parameters**

- `id` (`str`, required): The `msg_` send id whose trail to read.
- `limit` (`int`): Events per page, a whole number from 1 to 100, defaulting to 25.
- `cursor` (`str`): The `nextCursor` from the previous page, which is an event id. One that names no event of this send is a 400 `invalid_cursor`.
- `api_key` (`str`): Reads with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[EmailEventResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `object` set to `event`, `id`, `type`, `data` and `createdAt`.

**Example**

```python
from nextmail import nextmail

page = nextmail.emails.list_events('msg_3f9a1c07d2b84e6a9c5b1f20', limit=100)
bounces = [event['data'] for event in page['items'] if event['type'] == 'email.bounced']

print([event['type'] for event in page['items']], bounces, page['hasMore'])
```

**Notes**

- `email.delivered` is recorded in this trail but is never sent as a webhook, so a delivery report surfaces only here and in `recipients` on `get`.
- A send made with a test key records `email.sent` with `transport` set to `test` and `simulated` set to `True` in `data`.
- An unknown id is a 404, never an empty page.
- A tracked message opened or clicked many times records one event per counted hit, so its trail can run to many pages.

Also available in: API [`GET /emails/{id}/events`](https://nextmail.sh/docs/api/reference/emails#get-emails-id-events); TypeScript [`nextmail.emails.listEvents()`](https://nextmail.sh/docs/sdk/reference/emails#listEvents); Ruby [`emails.list_events`](https://nextmail.sh/docs/ruby/reference/emails#listEvents); PHP [`emails->listEvents`](https://nextmail.sh/docs/php/reference/emails#listEvents); Go [`Emails.ListEvents`](https://nextmail.sh/docs/go/reference/emails#listEvents); Java [`emails().listEvents`](https://nextmail.sh/docs/java/reference/emails#listEvents); C# [`Emails.ListEventsAsync`](https://nextmail.sh/docs/csharp/reference/emails#listEvents); CLI [`nextmail emails list-events`](https://nextmail.sh/docs/cli/reference/emails#emails-list-events).

### `emails.list_all_events()`

Collect the whole event trail of one sent email into one list

```python
def list_all_events(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[EmailEventResource]
```

Walks every page of one send's event trail and returns all of it in one list, oldest first: acceptance, scheduling, sending, every delivery report, bounce and complaint, and every counted open, click and download afterwards.

A widely read message can hold a great many events, all in memory before the call returns. Prefer `iterate_events` when you can stop early. `limit=` sets the page size of each request, not the total.

Scopes: `emails:read`.

**Parameters**

- `id` (`str`, required): The `msg_` send id whose trail to read.
- `limit` (`int`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`str`): An event id to start after, skipping every older event.
- `api_key` (`str`): Reads with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[EmailEventResource]` holding every event across all pages, oldest first.

**Example**

```python
from collections import Counter

from nextmail import nextmail

events = nextmail.emails.list_all_events('msg_3f9a1c07d2b84e6a9c5b1f20', limit=100)
counts = Counter(event['type'] for event in events)

print(len(events), counts['email.opened'], counts['email.clicked'])
```

**Notes**

- A failure on any page raises, and the events already fetched are discarded.
- An unknown id is a 404 on the first page.

Also available in: API [`GET /emails/{id}/events`](https://nextmail.sh/docs/api/reference/emails#get-emails-id-events); TypeScript [`nextmail.emails.listAllEvents()`](https://nextmail.sh/docs/sdk/reference/emails#listAllEvents); Ruby [`emails.list_all_events`](https://nextmail.sh/docs/ruby/reference/emails#listAllEvents); PHP [`emails->listAllEvents`](https://nextmail.sh/docs/php/reference/emails#listAllEvents); Go [`Emails.ListAllEvents`](https://nextmail.sh/docs/go/reference/emails#listAllEvents); Java [`emails().listAllEvents`](https://nextmail.sh/docs/java/reference/emails#listAllEvents); C# [`Emails.ListAllEventsAsync`](https://nextmail.sh/docs/csharp/reference/emails#listAllEvents).

### `emails.iterate_events()`

Stream the event trail of one sent email one event at a time

```python
def iterate_events(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[EmailEventResource]
```

Returns a generator over one send's event trail that yields events one at a time, oldest first, and fetches the next page only when the current one is drained. Nothing is requested until you consume it, and breaking out of the loop stops further requests.

The walk moves forward in time, so events recorded while you iterate are yielded when the walk reaches them, and it ends when `hasMore` is `False`.

Scopes: `emails:read`.

**Parameters**

- `id` (`str`, required): The `msg_` send id whose trail to read.
- `limit` (`int`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`str`): An event id to start after, skipping every older event.
- `api_key` (`str`): Reads with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[EmailEventResource]`, a generator that yields one event per step.

**Example**

```python
from nextmail import nextmail

for event in nextmail.emails.iterate_events('msg_3f9a1c07d2b84e6a9c5b1f20'):
    if event['type'] == 'email.delivered':
        print('Delivered at', event['createdAt'])
        break
```

**Notes**

- The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in: API [`GET /emails/{id}/events`](https://nextmail.sh/docs/api/reference/emails#get-emails-id-events); TypeScript [`nextmail.emails.iterateEvents()`](https://nextmail.sh/docs/sdk/reference/emails#iterateEvents); Ruby [`emails.iterate_events`](https://nextmail.sh/docs/ruby/reference/emails#iterateEvents); PHP [`emails->iterateEvents`](https://nextmail.sh/docs/php/reference/emails#iterateEvents); Go [`Emails.IterateEvents`](https://nextmail.sh/docs/go/reference/emails#iterateEvents); Java [`emails().iterateEvents`](https://nextmail.sh/docs/java/reference/emails#iterateEvents); C# [`Emails.IterateEventsAsync`](https://nextmail.sh/docs/csharp/reference/emails#iterateEvents).

### `emails.get_tracking()`

Read the engagement report for a sent email

```python
def get_tracking(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> TrackingResource
```

Returns the same document `tracking.get` serves, reached from the `msg_` id a sender already holds. It has the message totals, one entry per tracked copy under `recipients`, and every rewritten link with its clicks under `links`.

A message that was never tracked is a 404 here rather than an empty report. "We were not recording" and "nobody opened it" are different answers, and a client that renders them the same way makes a claim about a reader on no evidence. Tracking applies per send: `opens` and `clicks` record what was applied when the message went out, resolved from the setting of the address it was sent from (its own, else its domain catch-all's when the catch-all caught that address, else off) and any `tracking` override on the send, not what is switched on now.

Read every count as a floor. An open is inferred from a mail client fetching an image, so a reader whose client blocks images is never counted, and Gmail fetches the image once through its proxy and serves later views from cache. A click is stronger evidence than an open.

Scopes: `emails:read`.

**Parameters**

- `id` (`str`, required): The `msg_` send id returned by `send`.
- `api_key` (`str`): Reads with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`TrackingResource` with `object` set to `tracking`, `id` (the `tmsg_` tracking id), `sendId`, `opens`, `clicks`, `opened`, `clicked`, `attributable`, `openCount`, `clickCount`, `openCountRaw`, `clickCountRaw`, the first and last open and click times, `recipients` and `links`.

**Example**

```python
from nextmail import NextmailApiError, nextmail

try:
    report = nextmail.emails.get_tracking('msg_3f9a1c07d2b84e6a9c5b1f20')
except NextmailApiError as error:
    if error.is_not_found:
        print('Not tracked, so there is nothing to report')
    else:
        raise
else:
    readers = [
        entry['email']
        for entry in report['recipients']
        if entry['attributed'] and entry['openCount']
    ]

    print(report['openCount'], report['attributable'], readers)
```

**Notes**

- Only claim a named recipient has not opened when `attributable` is `True`. Mail that went to the whole list as one body has a shared copy whose `email` is `None`, and its opens cannot be pinned to anybody.
- A message sent with a test key is never tracked, so this is always a 404 for one.
- The gap between `openCountRaw` and `openCount` includes machine fetches such as Apple Mail Privacy Protection and repeats within thirty seconds, so do not read the difference as a pure machine count.

Also available in: API [`GET /emails/{id}/tracking`](https://nextmail.sh/docs/api/reference/tracking#get-emails-id-tracking); TypeScript [`nextmail.emails.getTracking()`](https://nextmail.sh/docs/sdk/reference/emails#getTracking); Ruby [`emails.get_tracking`](https://nextmail.sh/docs/ruby/reference/emails#getTracking); PHP [`emails->getTracking`](https://nextmail.sh/docs/php/reference/emails#getTracking); Go [`Emails.GetTracking`](https://nextmail.sh/docs/go/reference/emails#getTracking); Java [`emails().getTracking`](https://nextmail.sh/docs/java/reference/emails#getTracking); C# [`Emails.GetTrackingAsync`](https://nextmail.sh/docs/csharp/reference/emails#getTracking); CLI [`nextmail emails get-tracking`](https://nextmail.sh/docs/cli/reference/emails#emails-get-tracking).

### `emails.cancel()`

Stop a queued or scheduled email before it goes

```python
def cancel(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailResource
```

Cancels a message that has not been dispatched yet: a send held by `scheduledAt`, or an immediate send still inside its `cancellableForSeconds` undo window. The message moves to `cancelled`, nothing is delivered, and an `email.cancelled` event is recorded and sent to subscribed webhooks.

Only `queued` and `scheduled` messages can be cancelled. Once a message is `sending`, `sent`, `partial` or `failed` the call is a 409 `email_not_cancellable`, since there is no pending dispatch left to stop and mail that has gone cannot be recalled. An immediate send with no undo window is dispatched inside the `send` request, so by the time you hold its id it is usually past this point.

Cancelling is idempotent. Cancelling an already cancelled message returns the same cancelled message rather than an error, and the SDK retries the call after a network failure or a retryable status.

Scopes: `emails:send`.

**Parameters**

- `id` (`str`, required): The `msg_` send id to cancel.
- `api_key` (`str`): Cancels with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailResource` in its new state, with `status` set to `cancelled` and `scheduledAt` and `cancellableUntil` still showing when it would have gone.

**Example**

```python
from nextmail import nextmail

queued = nextmail.emails.send(
    {
        'from': 'billing@acme.com',
        'to': 'ada@example.com',
        'subject': 'Your September invoice',
        'text': 'Invoice attached.',
        'cancellableForSeconds': 30,
    }
)
cancelled = nextmail.emails.cancel(queued['id'])

print(cancelled['status'], cancelled['cancellableUntil'])
```

**Notes**

- `cancellableUntil` on the message is the moment it stops being cancellable. Past it, expect the 409.
- A cancelled message stays cancelled. There is no way to resume it, so send again if you change your mind.
- A narrowed key gets a 404 for a message sent from an address it does not cover.

Also available in: API [`POST /emails/{id}/cancel`](https://nextmail.sh/docs/api/reference/emails#post-emails-id-cancel); TypeScript [`nextmail.emails.cancel()`](https://nextmail.sh/docs/sdk/reference/emails#cancel); Ruby [`emails.cancel`](https://nextmail.sh/docs/ruby/reference/emails#cancel); PHP [`emails->cancel`](https://nextmail.sh/docs/php/reference/emails#cancel); Go [`Emails.Cancel`](https://nextmail.sh/docs/go/reference/emails#cancel); Java [`emails().cancel`](https://nextmail.sh/docs/java/reference/emails#cancel); C# [`Emails.CancelAsync`](https://nextmail.sh/docs/csharp/reference/emails#cancel); CLI [`nextmail emails cancel`](https://nextmail.sh/docs/cli/reference/emails#emails-cancel).

### `emails.reschedule()`

Move a queued or scheduled email to a new send time

```python
def reschedule(
    id: str,
    scheduled_at: datetime | str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailResource
```

Changes when a message that has not gone yet will be dispatched. `scheduledAt` is the only thing this call can change, and the SDK sends nothing else: the body, recipients and any translation stay exactly as they were accepted.

The new time takes a `datetime`, an ISO 8601 instant or a duration such as `PT30M` or `P2D`, measured from when the server receives the request. It must be at least one second in the future and at most 365 days out, otherwise the call is a 422 on `scheduledAt`, usually `invalid_parameter`. Earlier and later times are both allowed.

Only `queued` and `scheduled` messages can be moved. Anything already `sending`, `sent`, `partial`, `cancelled` or `failed` is a 409 `email_not_cancellable`. A queued message inside its undo window can be rescheduled too, which turns it into a `scheduled` send that stays cancellable until the new time.

Scopes: `emails:send`.

**Parameters**

- `id` (`str`, required): The `msg_` send id to move.
- `scheduled_at` (`datetime | str`, required): The new send time. A `datetime` is converted to UTC and sent as an ISO 8601 instant, and a naive one is taken as local time. A string is passed through as an instant or a duration.
- `api_key` (`str`): Reschedules with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailResource` with `status` set to `scheduled` and `scheduledAt` and `cancellableUntil` both set to the new time.

**Example**

```python
from datetime import datetime, timezone

from nextmail import nextmail

moved = nextmail.emails.reschedule(
    'msg_3f9a1c07d2b84e6a9c5b1f20', datetime(2026, 9, 16, 8, 0, tzinfo=timezone.utc)
)

print(moved['status'], moved['scheduledAt'])
```

**Notes**

- The SDK retries this call after a network failure. A duration is resolved again on each attempt, so a retried `PT1H` lands an hour after the last attempt the server received.
- An `email.rescheduled` event with the new `scheduledAt` is added to the trail on every successful move.
- A translated scheduled message keeps its approved wording. To change the text itself, cancel it and send again.

Also available in: API [`PATCH /emails/{id}`](https://nextmail.sh/docs/api/reference/emails#patch-emails-id); TypeScript [`nextmail.emails.reschedule()`](https://nextmail.sh/docs/sdk/reference/emails#reschedule); Ruby [`emails.reschedule`](https://nextmail.sh/docs/ruby/reference/emails#reschedule); PHP [`emails->reschedule`](https://nextmail.sh/docs/php/reference/emails#reschedule); Go [`Emails.Reschedule`](https://nextmail.sh/docs/go/reference/emails#reschedule); Java [`emails().reschedule`](https://nextmail.sh/docs/java/reference/emails#reschedule); C# [`Emails.RescheduleAsync`](https://nextmail.sh/docs/csharp/reference/emails#reschedule); CLI [`nextmail emails reschedule`](https://nextmail.sh/docs/cli/reference/emails#emails-reschedule).

### `emails.update()`

Change an email that has not gone yet

```python
def update(
    id: str,
    patch: EmailUpdate,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailResource
```

Changes a `queued` or `scheduled` message before it is dispatched: when it goes with `scheduledAt`, what it says with `subject`, `html` and `text`, the address it goes out as with `from`, and who it goes to with `to`, `cc` and `bcc`. Send any of them together, and a key you leave out keeps its value. This is what editing a scheduled message in the calendar of the app does.

A recipient list replaces the stored one whole, and takes a string such as `Ada <ada@example.com>`, a dict with `email` and `name`, or a list of either. `from` is checked as it is on a send, so it has to be an address the key may send as, or the call is a 403 `from_address_forbidden`.

Only messages that have not gone can change. Anything already `sending`, `sent`, `partial`, `cancelled` or `failed` is a 409 `email_not_cancellable`. A message translated when it was accepted keeps its approved wording, so a new `subject`, `html` or `text` on it is a 409 `translation_locked`, and one that was encrypted before it was scheduled keeps its wording and its recipients. Cancel those and send again instead.

Scopes: `emails:send`.

**Parameters**

- `id` (`str`, required): The `msg_` send id to change.
- `patch['scheduledAt']` (`datetime | str`): A new send time: a `datetime`, an ISO 8601 instant or a duration such as `PT2H`, in the future and at most 365 days out. A naive `datetime` is taken as local time.
- `patch['subject']` (`str`): The new subject, up to 998 characters.
- `patch['html']` (`str`): The new HTML body.
- `patch['text']` (`str`): The new plain text body.
- `patch['from']` (`str`): The address it goes out as instead, one the key may send as.
- `patch['to']` (`RecipientInput | list[RecipientInput]`): Replaces the recipients, at least one and at most 50 across the three lists.
- `patch['cc']` (`RecipientInput | list[RecipientInput]`): Replaces the copied recipients.
- `patch['bcc']` (`RecipientInput | list[RecipientInput]`): Replaces the blind copied recipients.
- `api_key` (`str`): Changes it with this key instead of the client's.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailResource`, the message as it is now. Its `subject`, `from` and `scheduledAt` show the change.

**Example**

```python
from datetime import datetime, timezone

from nextmail import nextmail

updated = nextmail.emails.update(
    'msg_3f9a1c07d2b84e6a9c5b1f20',
    {
        'subject': 'Your September invoice, corrected',
        'to': ['ada@example.com', 'grace@example.com'],
        'scheduledAt': datetime(2026, 10, 5, 8, 0, tzinfo=timezone.utc),
    },
)

print(updated['status'], updated['subject'], updated['scheduledAt'])
```

**Notes**

- The SDK retries this call after a network failure, which is safe because a repeat writes the same values.
- An `email.updated` event naming the changed fields is added to the trail, and a move adds `email.rescheduled` as well.
- `reschedule` is the same call with only `scheduledAt`.

Also available in: API [`PATCH /emails/{id}`](https://nextmail.sh/docs/api/reference/emails#patch-emails-id); TypeScript [`nextmail.emails.update()`](https://nextmail.sh/docs/sdk/reference/emails#update); Ruby [`emails.update`](https://nextmail.sh/docs/ruby/reference/emails#update); PHP [`emails->update`](https://nextmail.sh/docs/php/reference/emails#update); Go [`Emails.Update`](https://nextmail.sh/docs/go/reference/emails#update); Java [`emails().update`](https://nextmail.sh/docs/java/reference/emails#update); C# [`Emails.UpdateAsync`](https://nextmail.sh/docs/csharp/reference/emails#update); CLI [`nextmail emails update`](https://nextmail.sh/docs/cli/reference/emails#emails-update).

### `emails.compose()`

Write an email with AI

```python
def compose(
    body: EmailCompose,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailCompositionResource
```

Writes the body of an email from `prompt`, an instruction, a rough draft or a few notes, in the style of the mail this workspace has sent before, as the composer of the app does. `subject`, `to` and `cc` help the greeting and the tone fit, and `from`, the address it goes out as, has it written as that address.

Give `threadId` to write a reply: the messages of that thread are read as context, which also needs `threads:read`, and a key limited to particular addresses can only use a thread that arrived at them.

Give `draft`, the body written so far, and `prompt` says what to change in it, as the describe bar of the composer does. `withSubject: True` adds `subject` to the answer. Give `send`, the send options as they stand, even `{}`, and `prompt` may change the recipients, the send time, tracking, the signature and the language: the answer's `send` holds only what changed, in the shape `emails.send` takes, and `None` takes an option away. Add `send['fromOptions']`, the addresses it may go out as, and `prompt` may move it to one of them, which the answer names in `send['from']`. Add `send['encrypt']`, whether it is encrypted end to end as it stands, only when every recipient can receive encrypted mail, and `prompt` may turn encryption on or off. Times such as "tomorrow at 9" are read in `timeZone`, or the time zone of the account.

Give `templates: True` and `prompt` may switch the email to one of the workspace's published templates, which the answer names in `template`: send that template with `emails.send` in place of `body`, because its body replaces it. Give `template`, the id of the template the email uses now, and `prompt` leaves the body alone. Give `files: True` and `prompt` may attach files the workspace holds, found by their name: the answer lists them in `attach`, each ready to send as `{'fileId': file['id']}` in `attachments`, and names in `attachMissing` what matched no file. `templates` needs `templates:read` and `files` needs `files:read`, and each is ignored without its scope. A key limited to particular addresses only finds the files at those addresses.

Nothing is saved or sent. Pass `body` to `emails.send` or `drafts.create` when it reads right. Each call spends one of the workspace's AI actions, and a workspace that has used them all for the day is refused with a 429 `ai_quota_exceeded`.

Scopes: `emails:send`.

**Parameters**

- `body['prompt']` (`str`, required): What to write, up to 20,000 characters. With `draft`, what to change in it, and it may be empty to have the draft finished as it is.
- `body['subject']` (`str`): The subject so far, if there is one.
- `body['from']` (`str`): The address it goes out as, so it is written as that address.
- `body['template']` (`str`): The id of the template the email uses now, up to 128 characters. Its body is then the template, so `prompt` leaves the body alone.
- `body['templates']` (`bool`): Let `prompt` switch the email to one of the workspace's published templates, which the answer names in `template`. Needs `templates:read`, and without it this is ignored.
- `body['files']` (`bool`): Let `prompt` attach files the workspace holds, found by their name. The answer lists them in `attach`, each ready to send as `{'fileId': file['id']}`, and what matched no file in `attachMissing`. Needs `files:read`, and without it this is ignored.
- `body['withSubject']` (`bool`): Also write a subject. The answer then carries `subject`.
- `body['draft']` (`str`): The body written so far, as text or Markdown, up to 20,000 characters.
- `body['to']` (`list[str]`): Who it goes to, as a list of addresses.
- `body['cc']` (`list[str]`): Who is copied, as a list of addresses.
- `body['bcc']` (`list[str]`): Who is blind copied, as a list of addresses.
- `body['send']` (`EmailComposeSend`): The send options as they stand, a dict with `scheduledAt`, `tracking`, `signature`, `translate`, `fromOptions` and `encrypt`, each optional. Give it, even `{}`, to let `prompt` change them. `fromOptions` lists up to 100 addresses it may go out as, so `prompt` can move it to one of them, and `encrypt` says whether it is encrypted end to end: give it only when every recipient can receive encrypted mail, so `prompt` can turn encryption on or off.
- `body['timeZone']` (`str`): The IANA time zone to read times in, such as `Europe/Berlin`. Without it, the time zone of the account.
- `body['threadId']` (`str`): A thread to reply in, read as context.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailCompositionResource`, a dict with `object` set to `composition` and `body`, the text it wrote, plus `subject` when asked with `withSubject`, `send` when `send` was given, `template`, `attach` and `attachMissing` when `templates` or `files` was given and `prompt` asked for them, `note`, a short sentence when part of `prompt` cannot be done this way, such as adding somebody whose address it was not given, and `sources` when the draft drew on the knowledge base. Its `send` can carry `from`, the address to send it from instead, one of `send['fromOptions']`, and `encrypt`, whether to encrypt it end to end, when `send['encrypt']` was given. `template` is a dict with the `id` and `name` of the template to send it with instead, whose body replaces `body`. `attach` lists the files to attach, each a dict with `id`, `filename`, `contentType` and `size` in bytes, and `attachMissing` lists what `prompt` asked to attach that matched no file, as `prompt` named it. `sources` lists the knowledge base items it drew on, the pinned notes at the levels of the sending address and the items whose passages matched the request, each a dict with `id`, `title`, `kind` (`'note'`, `'file'` or `'link'`), `scope`, the level it sits at, and `url`, the page a link reads or `None`.

**Example**

```python
from nextmail import nextmail

composition = nextmail.emails.compose(
    {
        'prompt': 'Send Ada the signed contract and ask for the invoice by Friday.',
        'from': 'billing@acme.com',
        'to': ['ada@example.com'],
        'withSubject': True,
        'files': True,
    }
)

nextmail.emails.send(
    {
        'from': 'billing@acme.com',
        'to': ['ada@example.com'],
        'subject': composition.get('subject', 'The signed contract'),
        'text': composition['body'],
        'attachments': [{'fileId': file['id']} for file in composition.get('attach', [])],
    }
)
```

**Notes**

- The SDK does not retry it, because a second call writes something different and spends a second AI action.
- A server with no AI configured answers 409 `ai_not_configured`.

Also available in: API [`POST /emails/compose`](https://nextmail.sh/docs/api/reference/emails#post-emails-compose); TypeScript [`nextmail.emails.compose()`](https://nextmail.sh/docs/sdk/reference/emails#compose); Ruby [`emails.compose`](https://nextmail.sh/docs/ruby/reference/emails#compose); PHP [`emails->compose`](https://nextmail.sh/docs/php/reference/emails#compose); Go [`Emails.Compose`](https://nextmail.sh/docs/go/reference/emails#compose); Java [`emails().compose`](https://nextmail.sh/docs/java/reference/emails#compose); C# [`Emails.ComposeAsync`](https://nextmail.sh/docs/csharp/reference/emails#compose); CLI [`nextmail emails compose`](https://nextmail.sh/docs/cli/reference/emails#emails-compose).

### `emails.rewrite()`

Rewrite part of an email with AI

```python
def rewrite(
    body: EmailRewrite,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> EmailRewriteResource
```

Rewrites a subject or a body and returns different versions of it, as the rewrite menu of the composer does. `action` is `shorten`, `lengthen`, `rephrase`, `formal`, `casual` or `custom`, and `custom` needs `instruction` to say what to change, or the call is a 422 `invalid_parameter` on `instruction`. `target` is `body`, the default, or `subject`, and `count` asks for 1 to 5 versions, 3 by default.

Give `threadId` when the text is a reply, so the rewrite fits the conversation. That also needs `threads:read`. The versions never repeat the original and never add facts that are not in it.

Nothing is saved. Each call spends one of the workspace's AI actions.

Scopes: `emails:send`.

**Parameters**

- `body['text']` (`str`, required): The subject or body to rewrite.
- `body['action']` (`RewriteAction`, required): What to do: `shorten`, `lengthen`, `rephrase`, `formal`, `casual` or `custom`.
- `body['target']` (`RewriteTarget`): `body` or `subject`. Defaults to `body`.
- `body['instruction']` (`str`): What to change, up to 500 characters. Required with `custom`.
- `body['count']` (`int`): How many versions, 1 to 5. Defaults to 3.
- `body['threadId']` (`str`): The thread the text replies in, read as context.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`EmailRewriteResource`, a dict with `object` set to `rewrite`, `target` and `variations`, the list of versions.

**Example**

```python
from nextmail import nextmail

rewrite = nextmail.emails.rewrite(
    {
        'text': 'Hey, just checking whether you had a chance to look at the contract?',
        'action': 'formal',
    }
)

for version in rewrite['variations']:
    print(version)
```

**Notes**

- The SDK does not retry it, because a second call spends a second AI action.
- Fewer versions than `count` can come back when two of them turned out the same.

Also available in: API [`POST /emails/rewrite`](https://nextmail.sh/docs/api/reference/emails#post-emails-rewrite); TypeScript [`nextmail.emails.rewrite()`](https://nextmail.sh/docs/sdk/reference/emails#rewrite); Ruby [`emails.rewrite`](https://nextmail.sh/docs/ruby/reference/emails#rewrite); PHP [`emails->rewrite`](https://nextmail.sh/docs/php/reference/emails#rewrite); Go [`Emails.Rewrite`](https://nextmail.sh/docs/go/reference/emails#rewrite); Java [`emails().rewrite`](https://nextmail.sh/docs/java/reference/emails#rewrite); C# [`Emails.RewriteAsync`](https://nextmail.sh/docs/csharp/reference/emails#rewrite); CLI [`nextmail emails rewrite`](https://nextmail.sh/docs/cli/reference/emails#emails-rewrite).

### `emails.suggest_subject()`

Suggest a subject line

```python
def suggest_subject(
    body: EmailSubject,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> SubjectSuggestionResource
```

Reads the body of an email and returns a short subject for it, under 100 characters, in the style of the mail this workspace has sent, as the subject button of the composer does.

Nothing is saved. Each call spends one of the workspace's AI actions.

Scopes: `emails:send`.

**Parameters**

- `body['message']` (`str`, required): The body of the email, as text or HTML.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `NextmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`SubjectSuggestionResource`, a dict with `object` set to `subject_suggestion` and `subject`.

**Example**

```python
from nextmail import nextmail

suggestion = nextmail.emails.suggest_subject(
    {'message': 'Thanks for the signed contract. Could you send the invoice by Friday?'}
)

print(suggestion['subject'])
```

**Notes**

- The SDK does not retry it, because a second call spends a second AI action.

Also available in: API [`POST /emails/subject`](https://nextmail.sh/docs/api/reference/emails#post-emails-subject); TypeScript [`nextmail.emails.suggestSubject()`](https://nextmail.sh/docs/sdk/reference/emails#suggestSubject); Ruby [`emails.suggest_subject`](https://nextmail.sh/docs/ruby/reference/emails#suggestSubject); PHP [`emails->suggestSubject`](https://nextmail.sh/docs/php/reference/emails#suggestSubject); Go [`Emails.SuggestSubject`](https://nextmail.sh/docs/go/reference/emails#suggestSubject); Java [`emails().suggestSubject`](https://nextmail.sh/docs/java/reference/emails#suggestSubject); C# [`Emails.SuggestSubjectAsync`](https://nextmail.sh/docs/csharp/reference/emails#suggestSubject); CLI [`nextmail emails suggest-subject`](https://nextmail.sh/docs/cli/reference/emails#emails-suggest-subject).
