> ## Documentation Index
> Fetch the complete documentation index at: https://inkbox.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages

> Send, receive, and manage individual email messages in a mailbox, with cursor-based pagination and full body content on detail requests.

Messages represent individual emails in a mailbox. List and detail endpoints return message metadata; full body content (text/HTML) is fetched from storage on detail requests. All list endpoints use cursor-based pagination.

Message metadata includes `from_display_name`, the sender-supplied name from the
email's From header, or `null` when unavailable. It is limited to 255 characters
and does not verify the sender's identity. Keep `from_address` visible when
displaying the name. Older messages may have no display name.

The SDK field is `from_display_name` in Python and Rust, and `fromDisplayName` in
TypeScript. The CLI includes `fromDisplayName` in `inkbox email get` output.

`POST /messages` sends immediately. To save incomplete content, autosave changes,
or review attachments before sending, use [Drafts](/docs/api/mail/drafts). Drafts
do not appear in message, thread, or search results until sent.

***

## List messages

```text theme={null}
GET /mailboxes/{email_address}/messages
```

List messages in a mailbox, newest first, with cursor pagination.

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the mailbox |

### Query parameters

| Parameter | Type | Default | Description |
| :- | :- | :- | :- |
| `cursor` | string | — | Opaque cursor from a previous response's `next_cursor` |
| `limit` | integer | 50 | Results per page (1–100) |
| `direction` | string | — | Filter by `"inbound"` or `"outbound"`. Omit for all messages |
| `start_datetime` | string | — | Only messages created on or after this date/time |
| `end_datetime` | string | — | Only messages created on or before this date/time; a bare date includes the whole day |
| `tz` | string | UTC | IANA zone for interpreting zone-less `start_datetime`/`end_datetime` values. Defaults to UTC |

`start_datetime`, `end_datetime`, and `tz` filter on `created_at`. Both bounds are optional and non-breaking — omit them for unchanged behavior. See [Filtering by date](/docs/api/mail#filtering-by-date).

### Response (200)

```json JSON theme={null}
{
  "items": [
    {
      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "mailbox_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "thread_id": "d4e5f6a7-b8c9-0123-defa-234567890123",
      "message_id": "<20250301120000.abc123@inkboxmail.com>",
      "from_address": "agent-sales@inkboxmail.com",
      "to_addresses": ["customer@example.com"],
      "cc_addresses": null,
      "subject": "Your order is confirmed",
      "snippet": "Hi! Your order #1234 has been confirmed and will ship within...",
      "direction": "outbound",
      "status": "sent",
      "is_read": true,
      "is_starred": false,
      "has_attachments": false,
      "reply_to": null,
      "first_opened_at": null,
      "open_count": 0,
      "import_job_id": null,
      "created_at": "2025-03-01T12:00:00Z"
    }
  ],
  "next_cursor": "eyJ0cyI6ICIyMDI1LTAzLTAxVDEyOjAwOjAwWiIsICJpZCI6ICJjM2Q0ZTVmNiJ9",
  "has_more": true
}
```

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages?limit=25" \
    -H "X-API-Key: YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const mailboxId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages?limit=25`,
    { headers: { "X-API-Key": "YOUR_API_KEY" } }
  );
  const page = await response.json();
  // page.next_cursor contains the cursor for the next page
  ```

  ```python Python theme={null}
  import requests

  mailbox_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  response = requests.get(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages",
      headers={"X-API-Key": "YOUR_API_KEY"},
      params={"limit": 25},
  )
  page = response.json()
  # page["next_cursor"] contains the cursor for the next page
  ```
</CodeGroup>

***

## Get message

```text theme={null}
GET /mailboxes/{email_address}/messages/{message_id}
```

Get a single message with its full body content (plain text and HTML).

The body returned is the body Inkbox **stored**, which is not always the body the caller submitted: outbound `body_text`/`body_html` include any automatically applied custom signature and, while it applies, the "Sent via Inkbox" footer — see **Branding footer** under [Send message](#send-message).

> **Fetching marks inbound mail read.** A `GET` here with an **API key** marks an unread **inbound** message read — `is_read` comes back `true` in the same response. Human ([Console](https://inkbox.ai/console)) reads don't, and neither do the list, thread, or attachment routes, so an agent that only reads through those still needs the [flags PATCH](#update-message-flags) to mark read. This is distinct from `first_opened_at`, which reflects the **recipient** opening a message you sent (see [Open tracking](#open-tracking)).

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the mailbox |
| `message_id` | UUID | Unique identifier of the message |

### Response (200)

```json JSON theme={null}
{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "mailbox_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "thread_id": "d4e5f6a7-b8c9-0123-defa-234567890123",
  "message_id": "<20250301120000.abc123@inkboxmail.com>",
  "from_address": "agent-sales@inkboxmail.com",
  "to_addresses": ["customer@example.com"],
  "cc_addresses": null,
  "bcc_addresses": null,
  "subject": "Your order is confirmed",
  "snippet": "Hi! Your order #1234 has been confirmed and will ship within...",
  "direction": "outbound",
  "status": "sent",
  "is_read": true,
  "is_starred": false,
  "has_attachments": false,
  "reply_to": null,
  "first_opened_at": null,
  "open_count": 0,
  "import_job_id": null,
  "body_text": "Hi! Your order #1234 has been confirmed and will ship within 2 business days.",
  "body_html": "<p>Hi! Your order #1234 has been confirmed and will ship within 2 business days.</p>",
  "in_reply_to": null,
  "references": null,
  "attachment_metadata": null,
  "ses_message_id": "0102018abc123def-abcdef01-2345-6789-abcd-ef0123456789-000000",
  "created_at": "2025-03-01T12:00:00Z",
  "updated_at": "2025-03-01T12:00:00Z"
}
```

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID" \
    -H "X-API-Key: YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages/${messageId}`,
    { headers: { "X-API-Key": "YOUR_API_KEY" } }
  );
  const message = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages/{message_id}",
      headers={"X-API-Key": "YOUR_API_KEY"},
  )
  message = response.json()
  ```
</CodeGroup>

***

## Send message

```text theme={null}
POST /mailboxes/{email_address}/messages
```

Compose and send an email from a mailbox. Threading is automatically resolved from `in_reply_to_message_id`.

<Warning>
  **Storage limits**

  Every inbox has a storage cap set by your plan. A send that would push the inbox above its cap is refused with a `402` — from this endpoint, [reply-all](#reply-to-everyone), and [forward](#forward-message) alike — until space is freed (delete messages or threads) or the plan is upgraded. See [pricing](https://inkbox.ai/pricing) for per-plan caps.

  **Receiving is never blocked.** Inbound mail is always accepted, and still counts toward the inbox's usage. Current usage and the cap are on the [mailbox object](/docs/api/mail/mailboxes) as `storage_used_bytes` and `storage_limit_bytes`.
</Warning>

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the sending mailbox |

### Request body

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `recipients.to` | string\[] | No | Primary recipients |
| `recipients.cc` | string\[] | No | CC recipients |
| `recipients.bcc` | string\[] | No | BCC recipients (excluded from headers, delivered separately) |
| `subject` | string | Yes | Email subject line (max 998 chars) |
| `body_text` | string | No | Plain-text body |
| `body_html` | string | No | HTML body |
| `in_reply_to_message_id` | string | No | RFC 5322 Message-ID of the email being replied to |
| `reply_to` | string | No | Optional `Reply-To` address. If set, recipients' mail clients route replies here instead of the sending mailbox. |
| `attachments` | object\[] | No | File attachments (see below). Decoded attachments may total at most 25 MiB; the complete encoded message must also fit within 10 MiB. |
| `track_opens` | boolean | No | Embed an open-tracking pixel in the HTML body. Requires `body_html` — a plain-text-only send with `track_opens` returns `422`. Default `false`. See [Open tracking](#open-tracking). |

Provide at least one address across `recipients.to`, `recipients.cc`, and `recipients.bcc`. A message can have at most 50 distinct recipients across these lists; repeated addresses count once, ignoring case.

The complete MIME message, including headers, body, attachments, and encoding overhead, must not exceed 10 MiB (10,485,760 bytes). Base64 encoding increases attachment size, so an attachment below the 25 MiB decoded-attachment cap can still exceed the message limit.

**AttachmentUpload object:**

| Field | Type | Description |
| :- | :- | :- |
| `filename` | string | File name (max 255 chars) |
| `content_type` | string | MIME type (e.g. `application/pdf`) |
| `content_base64` | string | Base64-encoded file content |
| `content_id` | string \| null | Optional content ID (max 255 chars). Reference it in `body_html` as `cid:chart1` when the value is `chart1` to display the attachment inline. Omit it for a regular attachment. |

### Request example

```json JSON theme={null}
{
  "recipients": {
    "to": ["customer@example.com"],
    "cc": null,
    "bcc": null
  },
  "subject": "Your order is confirmed",
  "body_text": "Hi! Your order #1234 has been confirmed.",
  "body_html": "<p>Hi! Your order #1234 has been confirmed.</p>",
  "in_reply_to_message_id": null
}
```

### Response (201)

Returns the created message metadata (same as a list item — body content is not included).

### Custom signatures

When enabled and available for the sending mailbox, a
[custom signature](/docs/capabilities/email/signatures) is added to sends, replies,
reply-all, and forwards. Configure it through [Update mailbox](/docs/api/mail/mailboxes#update-mailbox),
not in each message request. The stored message includes the applied signature.
An HTML signature adds a formatted HTML version to a text-only message while
retaining the plain-text fallback. This does not enable open tracking. A text-only
signature does not create an HTML version; an existing HTML body keeps its formatting.
Original messages forwarded as attachments are unchanged. If the Inkbox branding
footer also applies, it appears after the custom signature.

### Idempotency

Send, [reply-all](#reply-to-everyone), and [forward](#forward-message) accept an optional `Idempotency-Key` header of up to 255 characters. Omit it and every call is an independent send.

Supply it and the key is bound to the email before it is handed off for delivery, so retrying after a lost or timed-out response cannot put a second copy of the email on the wire.

Also send `Prefer: idempotency-replay` to replay a completed request's original
HTTP status and response body. SDKs and CLI version 0.7.10 do this automatically.
Without that preference, completed keyed retries keep returning `409`.
Email success still waits for send acceptance and includes the email Message-ID.
The following table describes requests with the replay preference:

| Situation | Response |
| :- | :- |
| First call | `201` with the created message |
| Same key and input, original request completed | Original response and message ID |
| Same key with changed input | `409 idempotency_key_reused` |
| Same key, original request still processing | `409 idempotency_in_progress` with `Retry-After` |
| Same key, an earlier attempt's outcome is unresolved | `503 send_outcome_ambiguous` with `Retry-After` |
| Original response has expired or is no longer available | `409 result_unavailable` |

A conflict does not prove delivery. Follow its error code. If the outcome is
uncertain, try [send lookup](/docs/api/message-sends) or outbound history. An interrupted
send may have no recoverable ID. Missing confirmation is not permission to submit
another send. See the retry guide for response headers and confirmed rejections.

* **Keys are scoped to your organization, mailbox, and operation.** One key used for a send and for a forward does not collide. The same key on two mailboxes identifies two messages.
* **A retry does not spend quota twice.** It reuses the original attempt's recipient allowance rather than consuming it again.
* **Original responses can be replayed for seven days.** Do not intentionally recycle keys.
* **Input must match.** Changing recipients, content, or attachments under the same key returns a conflict.
* **Retry with the same key. Never mint a new one for the same email.** A new key is a new send. A saved rejection repeats the same error; changed content requires a new key. Validation errors before execution can leave the key unused. Missing confirmation never proves a message was not sent.

[Drafts](/docs/api/mail/drafts) retain their existing draft-and-generation retry contract.
Keys used without the replay preference retain their original scope and conflict behavior.

### Branding footer

By default, outbound mail carries a short "Sent via Inkbox" footer, appended to the text and HTML parts of every send, reply-all, and forward. Organizations on a [paid plan](https://inkbox.ai/pricing) can remove it — see [Branding footer](/docs/api/mail#branding-footer).

While it applies, the footer is written into the **stored** message, so it changes what the API gives back: a later [`GET /messages/{message_id}`](#get-message) returns `body_text` and `body_html` **with the footer appended** — not the exact bytes you submitted. A send with no body at all comes back with a body containing only the footer. Snippets and search text are computed from the body you submitted, so `snippet` and [search](/docs/api/mail/search) are unaffected.

### Open tracking

Set `track_opens` on a send or [forward](#forward-message) to embed an invisible tracking pixel in the HTML body. When the recipient's mail client loads it, the message reports two fields (present on every message response):

| Field | Type | Description |
| :- | :- | :- |
| `first_opened_at` | string \| null | ISO 8601 time of the first observed open, or `null` if none seen yet. This is the reliable signal. |
| `open_count` | integer | Approximate observed opens (default `0`). Automatic image fetching can increase the count, and multiple opens may be counted once. |

`track_opens` requires an HTML part — a plain-text-only send returns `422`. Tracking is per-message ("at least one observed open"), not per-recipient, and the pixel can raise spam scores, so a borderline message may be more likely to be filtered. `first_opened_at` (the recipient loaded the pixel) is distinct from `is_read` (your agent consumed the message via the API — see [Get message](#get-message)).

### Error responses

| Status | Description |
| :- | :- |
| 400 | Invalid attachment data, an attachment or message size limit was exceeded, or the request was rejected for policy reasons |
| 402 | Sending the message would exceed the inbox storage limit — delete messages or upgrade the plan |
| 403 | Organization mismatch, recipient restriction, conversation-health restriction, plan requirement, or a request that cannot fit within its effective limit |
| 409 | This `Idempotency-Key` already sent an email — see [Idempotency](#idempotency) |
| 422 | Invalid request fields, including an empty recipient set or more than 50 distinct recipients |
| 429 | A temporary organization send or new-recipient limit was reached. Retry timing is included when waiting can resolve it |
| 503 | An earlier attempt with this `Idempotency-Key` has an unresolved outcome, so no further send was attempted — see [Idempotency](#idempotency) |

Mail limits are shared by every mailbox in the organization and count distinct envelope recipients, not messages. The default fixed-window ceilings are 50 recipients per 10 minutes, 80 per hour, and 100 per 24 hours; plan limits may be higher. An address repeated across To, Cc, or Bcc counts once.

A retryable limit response uses structured detail:

```json theme={null}
{
  "detail": {
    "error": "mail_rate_limited",
    "message": "Sending limit reached: 80 recipients per hour for this organization.",
    "limit": 80,
    "window_seconds": 3600,
    "retry_after_seconds": 120
  }
}
```

If one request itself exceeds the effective ceiling, waiting cannot help. The API returns `403` with `detail.error` set to `mail_rate_limit_exceeded`, along with `limit`, `window_seconds`, and `recipient_count`, and omits `Retry-After`.

Other send controls use distinct `detail.error` values:

| Error | Status | What to do |
| :- | :- | :- |
| `conversation_unhealthy` | 403 | Do not retry unchanged. Follow the response's recipient-specific guidance before sending again. |
| `new_external_recipient_limit` | 429 or 403 | If `Retry-After` is present, wait that many seconds. Without it, reduce the request or change plans before retrying. |
| `custom_domain_sending_requires_paid_plan` | 403 | Use a platform-domain mailbox or change plans before retrying. |
| `unclaimed_agent_recipient_not_allowed` | 403 | An unclaimed agent may only email its own human. `detail.message` names that address; claim the agent to send anywhere else. |
| `unclaimed_agent_signup_missing` | 403 | Verify the agent or resend verification to restore restricted sending. |

Use the error code rather than the status alone to choose remediation. A `429`
with `Retry-After` is retryable after the stated delay; a `403` requires a change
to the request, recipient relationship, sending mailbox, or plan.

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "recipients": {"to": ["customer@example.com"]},
      "subject": "Your order is confirmed",
      "body_text": "Hi! Your order has been confirmed.",
      "body_html": "<p>Hi! Your order has been confirmed.</p>"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages`,
    {
        method: "POST",
        headers: {
            "X-API-Key": "YOUR_API_KEY",
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            recipients: { to: ["customer@example.com"] },
            subject: "Your order is confirmed",
            body_text: "Hi! Your order has been confirmed.",
            body_html: "<p>Hi! Your order has been confirmed.</p>",
        }),
    }
  );
  const message = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages",
      headers={"X-API-Key": "YOUR_API_KEY"},
      json={
          "recipients": {"to": ["customer@example.com"]},
          "subject": "Your order is confirmed",
          "body_text": "Hi! Your order has been confirmed.",
          "body_html": "<p>Hi! Your order has been confirmed.</p>",
      },
  )
  message = response.json()
  ```
</CodeGroup>

***

## Reply to everyone

[Companion replies](/docs/capabilities/companion-mode#reply-to-the-existing-conversation) use the same stored-message reply-all endpoint. The activation's `reply_context.reply_to_message_id` is a message UUID, not an RFC `Message-ID`. Preserve the sponsor and exact participant cohort. An out-of-cohort `Reply-To` is not silently rewritten; a reply requiring unavailable Companion permission returns `403` with `companion_reply_unavailable`. New recipient-only sends do not inherit sponsorship.

For an explicit-audience send, preserve `thread_id` using the canonical conversation ID and the exact `to`/`cc` audience. If supplying `in_reply_to_message_id`, use the stored parent's RFC `Message-ID`, never its UUID. The server still validates the existing reply relationship and cohort.

```text theme={null}
POST /mailboxes/{email_address}/messages/{message_id}/reply-all
```

Reply to every visible participant on a stored message. Recipients are resolved server-side from the original — you don't pass them. The reply stays in the original thread and counts against the same send rate limits as `POST /messages`.

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the replying mailbox |
| `message_id` | UUID | UUID of the message being replied to |

### Request body

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `subject` | string | No | Override; defaults to `"Re: " + original.subject` (idempotent — won't double-prefix). Max 998 chars. |
| `body_text` | string | No | Plain-text reply body. |
| `body_html` | string | No | HTML reply body. |
| `attachments` | object\[] | No | Base64-encoded attachments. Same `AttachmentUpload` shape and limits as Send. |
| `reply_to` | string | No | Optional `Reply-To` header on the reply. |

### Recipient resolution

* **Inbound original** — `To` is the original `Reply-To` if present, otherwise the original sender. `Cc` is the original `To` + `Cc`.
* **Outbound original** — `To` and `Cc` reuse the original `To` and `Cc`.
* In both cases the replying mailbox, duplicates, and the original BCC recipients are removed.

The message detail (`GET .../messages/{message_id}`) returns the resolved `reply_all_recipients` (`{ to, cc }`) so UIs can prefill editable fields.

### Request example

```json JSON theme={null}
{
  "body_text": "Thanks everyone — looping you all back in."
}
```

### Response (201)

Returns a `MessageResponse` — the brand-new message representing the reply (same shape as a list item; body content is not included).

Accepts an optional `Idempotency-Key` header, under its own key namespace — see [Idempotency](#idempotency).

### Error responses

| Status | Description |
| :- | :- |
| 400 | Attachment blocked/oversize |
| 402 | Sending the message would exceed the inbox storage limit — delete messages or upgrade the plan |
| 403 | Organization mismatch, contact-rule block, or unclaimed-agent recipient restriction |
| 404 | Mailbox or original message not found |
| 409 | This `Idempotency-Key` already sent an email — see [Idempotency](#idempotency) |
| 422 | No reply-all recipients could be resolved, or the message was rejected at send time |
| 429 | Send rate limit exceeded (same quota as `POST /messages`) |
| 503 | An earlier attempt with this `Idempotency-Key` has an unresolved outcome, so no further send was attempted — see [Idempotency](#idempotency) |

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID/reply-all" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "body_text": "Thanks everyone — looping you all back in."
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages/${messageId}/reply-all`,
    {
        method: "POST",
        headers: {
            "X-API-Key": "YOUR_API_KEY",
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            body_text: "Thanks everyone — looping you all back in.",
        }),
    }
  );
  const reply = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages/{message_id}/reply-all",
      headers={"X-API-Key": "YOUR_API_KEY"},
      json={
          "body_text": "Thanks everyone — looping you all back in.",
      },
  )
  reply = response.json()
  ```
</CodeGroup>

***

## Forward message

```text theme={null}
POST /mailboxes/{email_address}/messages/{message_id}/forward
```

Forward a stored message from this mailbox. Each forward creates a **new thread**.

Forwards count against the same send rate limits as `POST /messages` and fire a `message.forwarded` webhook event on success (not `message.sent`).

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the forwarding mailbox |
| `message_id` | UUID | UUID of the original message to forward |

### Request body

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `recipients.to` | string\[] | — | Primary recipients |
| `recipients.cc` | string\[] | — | CC recipients |
| `recipients.bcc` | string\[] | — | BCC recipients (excluded from headers, delivered separately) |
| `mode` | string | No | `"inline"` (default) or `"wrapped"`. See **Modes** below. |
| `subject` | string | No | Override; defaults to `"Fwd: " + original.subject` (idempotent — won't double-prefix if the original already starts with `Fwd:` / `Fw:`). An empty or missing original subject becomes `"Fwd: (no subject)"`. Max 998 chars. |
| `body_text` | string | No | Plain-text caller note prepended above the original body (inline) or included as a top-level note (wrapped). |
| `body_html` | string | No | HTML caller note. |
| `additional_attachments` | object\[] | No | Additional attachments. Same `AttachmentUpload` shape and 25 MiB decoded-attachment cap as Send; the complete encoded forward must also fit within 10 MiB. |
| `include_original_attachments` | boolean | No | `inline` mode only. Default `true` — original attachments are re-attached as direct outbound parts. Ignored in `wrapped` mode (originals live inside the wrapped `.eml`). |
| `reply_to` | string | No | Optional `Reply-To` header on the forward's outer envelope. The original's `Reply-To` (if any) is preserved inside the wrapped MIME but is not auto-inherited. |
| `track_opens` | boolean | No | Embed an open-tracking pixel; requires an HTML part on the outgoing forward. Inline mode inherits the original's HTML; wrapped mode needs a `body_html`. A no-HTML forward returns `422`. Default `false`. See [Open tracking](#open-tracking). |

At least one address is required across `recipients.to`, `recipients.cc`, and `recipients.bcc`. The combined limit is 50 distinct recipients, ignoring case.

### Modes

* **`inline`** (default): Renders the original body below a preamble containing its `From:`, `Date:`, `Subject:`, `To:`, and `Cc:` headers. Original attachments appear as direct attachments to the forward. Inline `cid:` images may break, and complex multipart structure is flattened.
* **`wrapped`**: Attaches the original message as a single `message/rfc822` part, preserving its headers, body parts, attachments, and nested structure. Recipients may see it inline or as a `forwarded.eml` attachment, depending on their mail client. Use this for originals with inline images, calendar invites, or complex multipart content. The result preserves the message's meaning and structure but may change its byte representation. DKIM-preserving forwarding is not supported.

### Forwarding behaviour

* Original-attachment bytes are excluded from the 25 MiB cap on additional attachments. The complete MIME message, including the original content and encoding overhead, must fit within 10 MiB.
* The forward gets a brand-new `Message-ID`. The original is referenced via an `X-Forwarded-Message-Id` header.
* Filename collisions across original attachments and caller `additional_attachments` are deduplicated automatically (caller wins; originals get a `forwarded-` prefix and a numeric suffix).

### Request example

```json JSON theme={null}
{
  "recipients": {"to": ["partner@example.com"]},
  "mode": "inline",
  "body_text": "Heads up — see below.",
  "include_original_attachments": true
}
```

### Response (201)

Returns a `MessageResponse` — the brand-new message representing the forward (same shape as a list item; body content is not included).

Accepts an optional `Idempotency-Key` header, under its own key namespace — see [Idempotency](#idempotency).

### Error responses

| Status | Description |
| :- | :- |
| 400 | Invalid attachment data, a size limit was exceeded, or the request was rejected for policy reasons |
| 402 | Sending the message would exceed the inbox storage limit — delete messages or upgrade the plan |
| 403 | Organization mismatch, contact-rule block, or unclaimed-agent recipient restriction |
| 404 | Mailbox or original message not found |
| 409 | This `Idempotency-Key` already sent an email — see [Idempotency](#idempotency) |
| 422 | Original message cannot be forwarded |
| 429 | Send rate limit exceeded (same quota as `POST /messages`) |
| 502 | Failed to load original body or attachments from storage |
| 503 | An earlier attempt with this `Idempotency-Key` has an unresolved outcome, so no further send was attempted — see [Idempotency](#idempotency) |

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID/forward" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "recipients": {"to": ["partner@example.com"]},
      "mode": "inline",
      "body_text": "Heads up — see below."
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages/${messageId}/forward`,
    {
        method: "POST",
        headers: {
            "X-API-Key": "YOUR_API_KEY",
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            recipients: { to: ["partner@example.com"] },
            mode: "inline",
            body_text: "Heads up — see below.",
        }),
    }
  );
  const forwarded = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages/{message_id}/forward",
      headers={"X-API-Key": "YOUR_API_KEY"},
      json={
          "recipients": {"to": ["partner@example.com"]},
          "mode": "inline",
          "body_text": "Heads up — see below.",
      },
  )
  forwarded = response.json()
  ```
</CodeGroup>

***

## Update message flags

```text theme={null}
PATCH /mailboxes/{email_address}/messages/{message_id}
```

Update `is_read` or `is_starred` on a message. Omit a field to leave it unchanged.

This is how to mark mail read in bulk, to mark a message unread, or to mark read when you process from the list or thread routes (which never mark read on their own). Fetching a single inbound message with an API key marks it read automatically — see [Get message](#get-message).

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the mailbox |
| `message_id` | UUID | Unique identifier of the message |

### Request body

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `is_read` | boolean | No | Mark as read (`true`) or unread (`false`) |
| `is_starred` | boolean | No | Star or unstar the message |

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"is_read": true}'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages/${messageId}`,
    {
        method: "PATCH",
        headers: {
            "X-API-Key": "YOUR_API_KEY",
            "Content-Type": "application/json",
        },
        body: JSON.stringify({ is_read: true }),
    }
  );
  const updated = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.patch(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages/{message_id}",
      headers={"X-API-Key": "YOUR_API_KEY"},
      json={"is_read": True},
  )
  updated = response.json()
  ```
</CodeGroup>

***

## Delete message

```text theme={null}
DELETE /mailboxes/{email_address}/messages/{message_id}
```

Delete a message. Returns `204 No Content` on success.

Deleting a message frees its storage immediately — the message and its associated storage overhead stop counting toward the inbox's storage cap right away.

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the mailbox |
| `message_id` | UUID | Unique identifier of the message |

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID" \
    -H "X-API-Key: YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages/${messageId}`,
    {
        method: "DELETE",
        headers: { "X-API-Key": "YOUR_API_KEY" },
    }
  );
  ```

  ```python Python theme={null}
  import requests

  requests.delete(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages/{message_id}",
      headers={"X-API-Key": "YOUR_API_KEY"},
  )
  ```
</CodeGroup>

***

## Download attachment

```text theme={null}
GET /mailboxes/{email_address}/messages/{message_id}/attachments/{filename}
```

Download an attachment from a message. By default, responds with a `302` redirect to a temporary download URL. Set `redirect=false` to get the URL as JSON instead.

### Path parameters

| Parameter | Type | Description |
| :- | :- | :- |
| `email_address` | string | Email address of the mailbox |
| `message_id` | UUID | Unique identifier of the message |
| `filename` | string | Attachment filename (must match exactly with `attachment_metadata[].filename`) |

### Query parameters

| Parameter | Type | Default | Description |
| :- | :- | :- | :- |
| `redirect` | boolean | `true` | `true` returns a `302` redirect to the download URL. `false` returns the URL as JSON. |

### Response (302) — redirect mode (default)

Redirects to a temporary download URL with a `Content-Disposition: attachment` header. The URL is valid for 15 minutes.

### Response (200) — JSON mode

```json JSON theme={null}
{
  "url": "https://downloads.example.com/messages/abc123/document.pdf?...",
  "filename": "document.pdf",
  "expires_in": 900
}
```

| Field | Type | Description |
| :- | :- | :- |
| `url` | string | Temporary download URL (valid for 15 minutes) |
| `filename` | string | Attachment filename |
| `expires_in` | integer | URL expiration time in seconds |

### Error responses

| Status | Description |
| :- | :- |
| 403 | Mailbox does not belong to your organization |
| 404 | Mailbox, message, or attachment not found |

### Code examples

<CodeGroup>
  ```bash cURL theme={null}
  # Redirect mode (default) — follows redirect to download
  curl -L "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID/attachments/document.pdf" \
    -H "X-API-Key: YOUR_API_KEY" \
    -o document.pdf

  # JSON mode — get the URL without redirecting
  curl "https://inkbox.ai/api/v1/mail/mailboxes/EMAIL_ADDRESS/messages/MESSAGE_ID/attachments/document.pdf?redirect=false" \
    -H "X-API-Key: YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  // JSON mode — get a temporary download URL
  const response = await fetch(
    `https://inkbox.ai/api/v1/mail/mailboxes/${emailAddress}/messages/${messageId}/attachments/document.pdf?redirect=false`,
    { headers: { "X-API-Key": "YOUR_API_KEY" } }
  );
  const { url, filename, expires_in } = await response.json();
  ```

  ```python Python theme={null}
  import requests

  # JSON mode
  response = requests.get(
      f"https://inkbox.ai/api/v1/mail/mailboxes/{email_address}/messages/{message_id}/attachments/document.pdf",
      headers={"X-API-Key": "YOUR_API_KEY"},
      params={"redirect": "false"},
  )
  data = response.json()  # {"url": "...", "filename": "...", "expires_in": 900}
  ```
</CodeGroup>

***

## Message object

### MessageResponse (list items)

| Field | Type | Description |
| :- | :- | :- |
| `id` | UUID | Unique message identifier |
| `mailbox_id` | UUID | Owning mailbox |
| `thread_id` | UUID \| null | Conversation thread, if resolved |
| `message_id` | string | RFC 5322 Message-ID header value |
| `from_address` | string | Sender address |
| `to_addresses` | string\[] | Primary recipients |
| `cc_addresses` | string\[] \| null | CC recipients |
| `subject` | string \| null | Email subject line |
| `snippet` | string \| null | First \~200 chars of the plain-text body |
| `direction` | string | `inbound` or `outbound` |
| `status` | string | `queued`, `sent`, `delivered`, `bounced`, `failed`, or `received` |
| `is_read` | boolean | Whether the message has been read |
| `is_starred` | boolean | Whether the message is starred |
| `has_attachments` | boolean | Whether the message has attachments |
| `reply_to` | string \| null | `Reply-To` header (inbound: parsed from the original; outbound: caller-supplied override) |
| `import_job_id` | UUID \| null | Mail import job that created this historical message, or `null` for normal mail |
| `created_at` | string | Creation timestamp (ISO 8601) |

### MessageDetailResponse (single message)

Includes all `MessageResponse` fields plus:

| Field | Type | Description |
| :- | :- | :- |
| `body_text` | string \| null | Plain-text body |
| `body_html` | string \| null | HTML body |
| `bcc_addresses` | string\[] \| null | BCC recipients |
| `in_reply_to` | string \| null | RFC 5322 In-Reply-To header |
| `references` | string\[] \| null | RFC 5322 References headers |
| `attachment_metadata` | object\[] \| null | Attachment descriptors for each file, including filename, content type, and size |
| `ses_message_id` | string \| null | Outbound provider message ID |
| `updated_at` | string | Last update timestamp (ISO 8601) |
| `reply_all_recipients` | object \| null | Server-suggested reply-all recipients (`{ to, cc }`), with the mailbox itself and BCC excluded — prefill editable To/Cc fields from this |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.