# Unified inbox endpoints

> Read, search, and triage incoming mail across every mailbox, then reply, label, snooze, and manage scheduled sends.

Source: https://docs.ayinbox.com/api/reference/unibox/

The unified inbox (unibox) is the org-wide view of everything that lands in your connected mailboxes. These endpoints power the dashboard's inbox list, thread view, scope rail, conversation labels, snoozes, and scheduled-send queue. Every route is gated on the unified-inbox feature, so the calling organization needs an active trial or paid subscription; without it the endpoint returns `403`.

The list, thread, overview, conversation-label, and snooze data is org-scoped, not per-user. Two members of the same organization see the same inbox, the same unread badge, the same threads, and the same conversation labels, whoever applied them. Snoozes are the one exception: they are attached to the calling user.

The list, thread, message, overview, unread-count and conversation-label reads are bounded at 30 seconds, including any wait for a database connection; a read that runs past it is abandoned and answers with the standard `500` error body.

List incoming mail [#list-incoming-mail]

`GET /unibox`

Returns the inbox list, collapsed to one row per thread (the newest message), with filtering and cursor pagination. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

| Parameter        | In    | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------- | ----- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cursor`         | query | string  | Opaque pagination cursor from a previous response.                                                                                                                                                                                                                                                                                                                                                     |
| `limit`          | query | integer | Page size. Clamped to the server's min/max.                                                                                                                                                                                                                                                                                                                                                            |
| `from`           | query | string  | Filter by sender address (substring).                                                                                                                                                                                                                                                                                                                                                                  |
| `address`        | query | string  | Conversations with this address in either direction: messages from it or sent to it (substring of the `From` or `To` entries).                                                                                                                                                                                                                                                                         |
| `direction`      | query | string  | `sent` returns only messages from one of your mailboxes, `received` only the rest. Omit for both.                                                                                                                                                                                                                                                                                                      |
| `campaign_id`    | query | string  | Campaign UUID. Returns only conversations tied to one of the campaign's sends: the sent copy, a reply naming it in `In-Reply-To`, and anything threaded with either. A malformed id returns `400`; a campaign outside your workspace returns `404`.                                                                                                                                                    |
| `contact_id`     | query | string  | Contact UUID. Returns the conversations tied to a campaign send to this contact, plus any mail from or to the contact's email address. Combined with `campaign_id`, only that campaign's conversations with the contact are returned, without the address match. A malformed id returns `400`; a contact outside your workspace returns `404`.                                                         |
| `folder`         | query | string  | One of `inbox`, `sent`, `drafts`, `archive`, `spam`, `trash`. Omit for every folder except `spam` and `trash` (junk never bleeds into the combined view). An unknown value returns `400`.                                                                                                                                                                                                              |
| `subject`        | query | string  | Filter by subject (substring).                                                                                                                                                                                                                                                                                                                                                                         |
| `unseen`         | query | boolean | `true` returns only threads with unread messages.                                                                                                                                                                                                                                                                                                                                                      |
| `awaiting_reply` | query | boolean | `true` returns only threads where the latest message was sent by you (recipient has not replied).                                                                                                                                                                                                                                                                                                      |
| `snoozed`        | query | string  | `true` returns only snoozed threads. Omit to exclude snoozed threads (default).                                                                                                                                                                                                                                                                                                                        |
| `since`          | query | string  | Earliest message date, inclusive. A date (`YYYY-MM-DD`) starts at 00:00 UTC that day; an RFC 3339 timestamp (`2026-06-11T09:00:00+02:00`) is used exactly.                                                                                                                                                                                                                                             |
| `until`          | query | string  | Latest message date. A date (`YYYY-MM-DD`) includes that whole UTC day; an RFC 3339 timestamp is an exclusive bound, so a message at exactly that instant is left out.                                                                                                                                                                                                                                 |
| `email_id`       | query | string  | Restrict to a single mailbox by UUID.                                                                                                                                                                                                                                                                                                                                                                  |
| `email_ids`      | query | string  | Comma-separated mailbox UUIDs. A thread matches if it landed in any of them. Invalid UUIDs are dropped.                                                                                                                                                                                                                                                                                                |
| `category_ids`   | query | string  | Comma-separated conversation-label UUIDs. A thread matches if it carries any of them.                                                                                                                                                                                                                                                                                                                  |
| `uncategorized`  | query | boolean | `true` returns only threads carrying no conversation labels.                                                                                                                                                                                                                                                                                                                                           |
| `automated`      | query | boolean | `true` returns only conversations no person wrote in: security alerts, notifications, bounces and auto-replies, as judged by [automatic inbox tagging](/guides/inbox-tagging/). `false` leaves them out, which is how the dashboard's Inbox reads. Omit for both. A conversation counts as automated only while every message in it that is not yours was judged automated, so a reply brings it back. |

The response is a `data` plus `pagination` envelope. Each row summarises the whole thread behind it (`message_count`, `has_unread`) plus the conversation's labels. When one of the thread's messages is a campaign send or names one, the row carries that campaign's `campaign_id` (the latest such message wins; with a `campaign_id` filter, that campaign). Rows with no campaign send omit the field.

`since` and `until` take a `YYYY-MM-DD` date or an RFC 3339 timestamp, and dates are read in UTC. Any other value returns `400` with code `invalid_filter`, as does a `campaign_id` or `contact_id` that is not a UUID. A window that ends before it starts matches nothing.

A conversation counts toward `campaign_id` and `contact_id` through the campaign sends recorded for them. A send whose contact has since been deleted is no longer linked to any campaign conversation.

Response [#response]

```json
{
  "data": [
    {
      "id": "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
      "email_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "thread_id": "thread-af83b21",
      "from_addr": ["Jane Doe <jane@acme.com>"],
      "to_addr": ["sales@yourco.com"],
      "subject": "Re: Following up on pricing",
      "snippet": "Thanks for the details, this looks great...",
      "internal_date": "2026-06-11T14:22:09Z",
      "seen": false,
      "message_count": 4,
      "has_unread": true,
      "labels": [
        { "id": "c0ffee00-0000-4000-8000-000000000001", "title": "Interested", "color": "#16a34a" }
      ],
      "campaign_id": "5d1e2f3a-4b5c-4d6e-8f70-81a2b3c4d5e6"
    }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6Ii4uLiJ9",
    "has_more": true
  }
}
```

Get unread count [#get-unread-count]

`GET /unibox/count`

Returns the number of unread conversations in the Inbox folder, optionally scoped to one mailbox. Snoozed conversations, automated conversations (see `automated` above) and mail in other folders (sent, drafts, archive, spam, trash) are not counted, so the number matches what the Inbox view lists as unread. Backs the inbox badge. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

| Parameter  | In    | Type   | Description                                                 |
| ---------- | ----- | ------ | ----------------------------------------------------------- |
| `email_id` | query | string | Optional mailbox UUID to count unread for a single mailbox. |

Response [#response-1]

```json
{
  "count": 37
}
```

Get inbox overview [#get-inbox-overview]

`GET /unibox/overview`

Rolls up the scope rail and top metric strip in one call: unread, today, week, snoozed, awaiting-reply, draft (`drafts`, each visible thread once whether it holds a mailbox draft or a pending agent draft), and pending-scheduled counts, plus per-folder, per-mailbox, per-tag, and per-conversation-label breakdowns. The `folders` array always lists all six canonical folders, zero-filled, in sidebar order; the headline counts exclude `spam` and `trash`. All counts are threads, not messages, except `scheduled_pending`, which counts the sends queued from the organization's mailboxes against the workspace cap in `scheduled_pending_max`. Automated conversations are counted in `automated` and `automated_unread` and left out of `unread`, `today`, `week`, the `inbox` folder and the per-mailbox and per-tag counts; `total` and the label counts still include them. Concurrent requests from the same organization share one computation, and its result is served for up to one second after it started (`generated_at` is when it started). Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

Response [#response-2]

```json
{
  "total": 1284,
  "unread": 37,
  "today": 12,
  "week": 88,
  "snoozed": 3,
  "awaiting_reply": 9,
  "automated": 41,
  "automated_unread": 6,
  "drafts": 4,
  "scheduled_pending": 2,
  "scheduled_pending_max": 10000,
  "folders": [
    { "folder": "inbox", "unread": 31, "total": 812 },
    { "folder": "sent", "unread": 0, "total": 402 },
    { "folder": "drafts", "unread": 0, "total": 4 },
    { "folder": "archive", "unread": 6, "total": 66 },
    { "folder": "spam", "unread": 2, "total": 9 },
    { "folder": "trash", "unread": 0, "total": 3 }
  ],
  "mailboxes": [
    {
      "id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "email": "sales@yourco.com",
      "name": "Sales",
      "unread": 21,
      "total": 640
    }
  ],
  "tags": [
    {
      "id": "1f2e3d4c-5b6a-7980-a1b2-c3d4e5f60718",
      "title": "Outbound",
      "color": "#2563eb",
      "unread": 14,
      "total": 410
    }
  ],
  "categories": [
    {
      "id": "c0ffee00-0000-4000-8000-000000000001",
      "title": "Interested",
      "color": "#16a34a",
      "unread": 5,
      "total": 62
    }
  ],
  "generated_at": "2026-06-11T14:25:00Z",
  "window_today_start": "2026-06-11T00:00:00Z",
  "window_week_start": "2026-06-05T00:00:00Z"
}
```

Get a thread [#get-a-thread]

`GET /unibox/thread`

Returns every message in a single conversation, oldest-first style message rows, with cursor pagination. The mailbox filter is optional: with no `email_id` the thread is read across every mailbox in the organization (the natural unified view). Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

| Parameter   | In    | Type    | Description                                                                         |
| ----------- | ----- | ------- | ----------------------------------------------------------------------------------- |
| `thread_id` | query | string  | Required. The thread to read. Also accepted as `id`.                                |
| `email_id`  | query | string  | Optional mailbox UUID to scope the thread to one mailbox. Also accepted as `email`. |
| `cursor`    | query | string  | Opaque pagination cursor.                                                           |
| `limit`     | query | integer | Page size. Out-of-range values return `400`.                                        |

The response is a `data` plus `pagination` envelope. Each item is a full message (envelope plus body). A message that answers an email sent from another workspace mailbox, as a reply in a [shared reply inbox](/guides/mailboxes/#a-shared-reply-inbox) does, carries that mailbox's id in `answers_mailbox_id`; the field is absent otherwise.

Response [#response-3]

```json
{
  "data": [
    {
      "id": "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
      "email_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "mailbox": 1,
      "thread_id": "thread-af83b21",
      "message_id": "<CA+abc123@mail.acme.com>",
      "gmail_id": "18f0c2a9b7d4e5f6",
      "parent_id": "<CA+prev@mail.acme.com>",
      "uid": 4821,
      "mod_seq": 90210,
      "flags": ["\\Seen"],
      "bcc": [],
      "cc": [],
      "from_addr": ["Jane Doe <jane@acme.com>"],
      "in_reply_to": ["<CA+prev@mail.acme.com>"],
      "reply_to": [],
      "to_addr": ["sales@yourco.com"],
      "answers_mailbox_id": "5e11d0c4-0000-0000-0000-000000000003",
      "subject": "Re: Following up on pricing",
      "size": 18422,
      "internal_date": "2026-06-11T14:22:09Z",
      "sent_date": "2026-06-11T14:22:00Z",
      "snippet": "Thanks for the details, this looks great...",
      "seen": true,
      "body_plain": "Thanks for the details...",
      "body_html": "<p>Thanks for the details...</p>",
      "updated_at": "2026-06-11T14:22:10Z",
      "created_at": "2026-06-11T14:22:10Z"
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  }
}
```

Get thread labels [#get-thread-labels]

`GET /unibox/thread/labels`

Returns the labels attached to a thread, drawn from the same workspace list as contact labels (managed through `/categories`). Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

| Parameter   | In    | Type   | Description                                                     |
| ----------- | ----- | ------ | --------------------------------------------------------------- |
| `thread_id` | query | string | Required. The thread to read labels for. Also accepted as `id`. |

The response wraps the labels in a `data` array.

Response [#response-4]

```json
{
  "data": [
    { "id": "c0ffee00-0000-4000-8000-000000000001", "title": "Interested", "color": "#16a34a" }
  ]
}
```

Set thread labels [#set-thread-labels]

`PUT /unibox/thread/labels`

Replaces the full conversation-label set on a thread, for the whole workspace. The body's `category_ids` is the desired set, so the call is idempotent and retries are naturally safe. Only the workspace's own labels are attached; an id belonging to another organization is dropped. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

Request body [#request-body]

| Field          | Type      | Required | Description                                                            |
| -------------- | --------- | -------- | ---------------------------------------------------------------------- |
| `thread_id`    | string    | Yes      | The thread to label.                                                   |
| `category_ids` | string\[] | No       | The full desired set of label UUIDs. An empty array clears all labels. |

```json
{
  "thread_id": "thread-af83b21",
  "category_ids": [
    "c0ffee00-0000-4000-8000-000000000001",
    "c0ffee00-0000-4000-8000-000000000002"
  ]
}
```

Response [#response-5]

Returns the resulting label set in a `data` array.

```json
{
  "data": [
    { "id": "c0ffee00-0000-4000-8000-000000000001", "title": "Interested", "color": "#16a34a" },
    { "id": "c0ffee00-0000-4000-8000-000000000002", "title": "Demo booked", "color": "#7c3aed" }
  ]
}
```

Mark messages seen [#mark-messages-seen]

`PATCH /unibox/seen`

Marks messages as read or unread, org-wide: an explicit batch of up to 500 message ids, up to 500 conversations, or a whole canonical folder at once. `email_ids` and `thread_ids` can be sent together; `folder` cannot be combined with either and returns `400`. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

Marking a conversation read reads every message in it. Marking one unread marks only its newest received message unread (one outside spam and trash when there is one). If it has no received message, its newest sent copy turns unread instead; otherwise sent copies stay read. Drafts are never marked unread. Explicit `email_ids` and `folder` sweeps never mark sent or draft copies unread.

The change is also carried out to the mailbox itself, so a message marked read here stops showing as unread in Gmail, Outlook or an IMAP mailbox. Opening a message with `GET /unibox/:id` marks it read the same way, relay included. Only messages whose state actually changed are relayed, and the relay is best-effort: it happens after the response, on the worker holding the mailbox, and a mailbox that is unplaced at that moment keeps its own read state until something changes it again. [Filing a conversation](/guides/unibox/#filing-in-the-mailbox-too) is relayed the same way.

Request body [#request-body-1]

| Field        | Type      | Required | Description                                                                                                             |
| ------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `email_ids`  | string\[] | No       | Message UUIDs to update (max 500).                                                                                      |
| `thread_ids` | string\[] | No       | Conversations to update (max 500).                                                                                      |
| `folder`     | string    | No       | Sweep every message in this folder instead of an id list: one of `inbox`, `sent`, `drafts`, `archive`, `spam`, `trash`. |
| `seen`       | boolean   | No       | `true` marks as read, `false` marks as unread.                                                                          |

```json
{
  "email_ids": [
    "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
    "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
  ],
  "seen": true
}
```

Response [#response-6]

Echoes the request back.

```json
{
  "email_ids": [
    "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
    "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
  ],
  "seen": true
}
```

Reply from the inbox [#reply-from-the-inbox]

`POST /unibox/reply`

Sends or schedules a reply or a forward from any mailbox in the organization. The send is routed through the per-mailbox scheduler according to `send_mode`. Requires an active organization. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

Request body [#request-body-2]

| Field                | Type      | Required | Description                                                                                                                         |
| -------------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `email_account_id`   | string    | Yes      | UUID of the sending mailbox. Any active mailbox in the organization, not only the one holding the thread.                           |
| `to`                 | string\[] | Yes      | Recipient addresses (at least one).                                                                                                 |
| `cc`                 | string\[] | No       | CC addresses.                                                                                                                       |
| `bcc`                | string\[] | No       | BCC addresses.                                                                                                                      |
| `subject`            | string    | Yes      | Subject line.                                                                                                                       |
| `body_html`          | string    | No       | HTML body.                                                                                                                          |
| `body_plain`         | string    | No       | Plain-text body. Leave it empty with `body_html` set and the plain-text part is rendered from the HTML, link destinations included. |
| `in_reply_to`        | string\[] | No       | Message-ID(s) this reply threads under. When omitted, the newest Message-ID in `thread_id` is used.                                 |
| `thread_id`          | string    | No       | The conversation being answered. Not used with `forward_message_id`; see [Forwarding a message](#forwarding-a-message).             |
| `send_mode`          | string    | No       | `instant` (default), `smart` (next mailbox gap), or `scheduled` (use `scheduled_at`).                                               |
| `scheduled_at`       | string    | No       | RFC 3339 timestamp. Required when `send_mode` is `scheduled`; must be in the future.                                                |
| `forward_message_id` | string    | No       | UUID of a stored message (the `id` from `GET /unibox/:id`) to forward. See [Forwarding a message](#forwarding-a-message).           |

```json
{
  "email_account_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "to": ["jane@acme.com"],
  "subject": "Re: Following up on pricing",
  "body_html": "<p>Happy to hop on a call this week.</p>",
  "in_reply_to": ["<CA+abc123@mail.acme.com>"],
  "thread_id": "thread-af83b21",
  "send_mode": "instant"
}
```

Response [#response-7]

```json
{
  "task_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
  "scheduled_at": "2026-06-11T14:30:00Z",
  "send_mode": "instant"
}
```

Instant sends are held for the sender's undo window (5 to 120 seconds, default 30) before they actually leave, so `scheduled_at` is that far in the future. Within the window the send can still be cancelled with `DELETE /unibox/scheduled/:task_id`.

A reply to an address on the workspace [suppression list](/guides/unsubscribe/#the-suppression-list), directly or through its domain, answers `400` with `code: "recipient_suppressed"` and a `message` naming the address, and nothing is queued. A forward is not checked against the list.

Only Gmail has a provider thread handle, and it belongs to the mailbox that holds the thread. So the reply files into the Gmail thread `thread_id` names only when `email_account_id` holds a message in it; a mailbox that already replied into the conversation from outside continues the thread that reply started. Otherwise it goes out with the same In-Reply-To and References headers and the conversation's subject, which keeps it in the same conversation for the recipient, and starts a conversation of its own in the sending mailbox. The queued send still belongs to `thread_id`, so it is listed with that conversation's scheduled sends.

Forwarding a message [#forwarding-a-message]

Set `forward_message_id` to forward a message from the inbox. AY Inbox attaches the stored message itself, so `body_html` and `body_plain` are an optional note and can be left empty. The message is copied when the request is accepted, so a scheduled forward still carries it if the original is later deleted. What goes out, in both the HTML and the plain-text part:

1. your note, if any
2. the sending mailbox's signature, when signature sync is on (a forward is signed even without a note)
3. a `---------- Forwarded message ---------` block with the original From, Date, Subject, To and Cc lines, then the original body

The Date line is written in the sending mailbox's time zone (its own, else the workspace's, else UTC). The original HTML keeps its formatting: its stylesheet is folded into inline styles and it is sanitized like a message opened in Unibox, so scripts, event handlers and AY Inbox's own open-tracking pixels are dropped. When the original has only a text part, it is forwarded as text. Links in the forwarded message are never rewritten for click tracking; only your note is tracked, when the mailbox tracks direct mail. Forwarding one of your own tracked sends (from Sent, say) swaps its tracked links back to their real destinations, so the new recipient's clicks are not counted against the original send.

A forward goes out as a new conversation for its recipient: no provider thread, and no In-Reply-To or References unless you set `in_reply_to` yourself. A `thread_id` sent with it is not used for either. Inside Unibox the forward belongs to the conversation of the message it forwards (that message's `thread_id`), whichever mailbox sends it: it is listed with that conversation's scheduled sends while queued, and once the sending mailbox's Sent folder syncs, the sent copy is returned by `GET /unibox/thread` for that conversation, stored once. A forward in a conversation is never picked as the parent Message-ID for a later reply into it.

The [suppression list](/guides/unsubscribe/#the-suppression-list) does not apply to a forward: it passes on mail already received to recipients chosen by hand, so it may go to an address or domain the list keeps out of campaigns. Fair use still counts its recipients.

Two limits:

* attachments on the original are not forwarded
* a message whose full body is not stored (the reader shows "Only a preview of this message is stored") is forwarded with that preview

Forwarding discloses the message, so it takes `READ_UNIBOX` as well as `WRITE_UNIBOX` (for a dashboard session, the `access_unibox` permission covers both), and an API key restricted to certain mailboxes must be allowed the mailbox the message belongs to as well as the sending mailbox. Either missing answers `403`. A `forward_message_id` that names no message in the organization answers `404`.

```json
{
  "email_account_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "to": ["finance@acme.com"],
  "subject": "Fwd: Following up on pricing",
  "body_plain": "Can you take a look at this?",
  "forward_message_id": "7f8e9d0c-1b2a-4c3d-9e8f-0a1b2c3d4e5f"
}
```

List active snoozes [#list-active-snoozes]

`GET /unibox/snoozes`

Returns your active thread snoozes. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

The response wraps the snoozes in a `data` array.

Response [#response-8]

```json
{
  "data": [
    {
      "id": "3a4b5c6d-7e8f-4a0b-9c1d-2e3f4a5b6c7d",
      "user_id": "1e2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
      "thread_id": "thread-af83b21",
      "snoozed_until": "2026-06-12T09:00:00Z",
      "created_at": "2026-06-11T14:00:00Z",
      "updated_at": "2026-06-11T14:00:00Z"
    }
  ]
}
```

Snooze a thread [#snooze-a-thread]

`POST /unibox/snooze`

Hides a thread from your inbox until `snoozed_until` passes. Upsert semantics: a second call on the same thread updates the time in place. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

Request body [#request-body-3]

| Field           | Type   | Required | Description                               |
| --------------- | ------ | -------- | ----------------------------------------- |
| `thread_id`     | string | Yes      | The thread to snooze.                     |
| `snoozed_until` | string | Yes      | RFC 3339 timestamp to un-hide the thread. |

```json
{
  "thread_id": "thread-af83b21",
  "snoozed_until": "2026-06-12T09:00:00Z"
}
```

Response [#response-9]

```json
{
  "id": "3a4b5c6d-7e8f-4a0b-9c1d-2e3f4a5b6c7d",
  "user_id": "1e2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "thread_id": "thread-af83b21",
  "snoozed_until": "2026-06-12T09:00:00Z",
  "created_at": "2026-06-11T14:00:00Z",
  "updated_at": "2026-06-11T14:00:00Z"
}
```

Unsnooze a thread [#unsnooze-a-thread]

`DELETE /unibox/snooze`

Un-snoozes a thread immediately. Idempotent: deleting a snooze that does not exist still succeeds with `204`. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

| Parameter   | In    | Type   | Description                        |
| ----------- | ----- | ------ | ---------------------------------- |
| `thread_id` | query | string | Required. The thread to un-snooze. |

Response [#response-10]

`204 No Content` with an empty body.

List scheduled sends [#list-scheduled-sends]

`GET /unibox/scheduled`

Returns the outbound emails queued from the organization's mailboxes but not yet sent, whichever member queued them. An API key limited to certain mailboxes sees only their sends. Pass `thread_id` to scope to a single conversation (used to render queued replies inline); the response shape is identical either way. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

| Parameter   | In    | Type   | Description                                                   |
| ----------- | ----- | ------ | ------------------------------------------------------------- |
| `thread_id` | query | string | Optional. Restrict to scheduled sends queued into one thread. |

The response wraps the items in a `data` array. Each item is a preview of the queued message.

Response [#response-11]

```json
{
  "data": [
    {
      "task_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
      "scheduled_at": "2026-06-12T09:15:00Z",
      "created_at": "2026-06-11T14:30:00Z",
      "account_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "account_email": "sales@yourco.com",
      "account_name": "Sales",
      "to": ["jane@acme.com"],
      "subject": "Re: Following up on pricing",
      "snippet": "Happy to hop on a call this week.",
      "thread_id": "thread-af83b21"
    }
  ]
}
```

Cancel a scheduled send [#cancel-a-scheduled-send]

`DELETE /unibox/scheduled/:task_id`

Cancels a pending scheduled send from any of the organization's mailboxes (for an API key limited to certain mailboxes, from those) before it fires. The queued task is marked cancelled and short-circuits to a no-op when its run time arrives. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

| Parameter | In   | Type   | Description                           |
| --------- | ---- | ------ | ------------------------------------- |
| `task_id` | path | string | UUID of the scheduled task to cancel. |

Response [#response-12]

`204 No Content` with an empty body. Only a send that is still pending can be cancelled, and a cancelled send is never sent, even when its queued run arrives later.

| Status | `code`                       | When                                                                                               |
| ------ | ---------------------------- | -------------------------------------------------------------------------------------------------- |
| `404`  | `not_found`                  | No scheduled send with that id is reachable from the workspace (or from the API key's mailboxes).  |
| `409`  | `scheduled_send_not_pending` | The send is no longer pending: it has started sending, was sent, failed, or was already cancelled. |

Auto-reply runs [#auto-reply-runs]

What [auto-reply](/guides/auto-reply/) did with each reply it took, newest first. The scope map and the campaign routes are in [endpoints](/api/endpoints/#auto-reply).

`GET /auto-reply/runs` lists runs, filtered by `thread_id`, `campaign_id`, `contact_id` and `status` (`queued`, `scheduled`, `dispatching`, `sent`, `failed`, `unknown`, `handed_off`, `skipped`, `cancelled`), with `limit` (1 to 100) and an opaque `cursor`. Bodies are left out. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

`GET /auto-reply/runs/:id` returns one run with its `body`, `reason` and `check_results`. `check_results` holds `violations` and `judge` from the check at writing, `send_time_violations` when the check just before sending refused it, and `send_admission` (for example `RECIPIENT_SUPPRESSED`) when the send path refused it. A run handed off before it was charged carries no `body`. Same auth.

`POST /auto-reply/runs/:id/cancel` stops a run before its send is reserved. Body `{"keep_draft": true}` turns the answer into a pending draft; `reason` may be `member_cancelled` or `member_composing`. Cancelling a cancelled run succeeds. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.

`GET /auto-reply/settings`, `POST /auto-reply/enable` and `POST /auto-reply/pause` read and set the workspace switch. Turning it on needs `SEND_CAMPAIGNS`; pausing needs `WRITE_UNIBOX`.

Get a message by id [#get-a-message-by-id]

`GET /unibox/:id`

Returns a single message by its UUID, including the full envelope and body. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.

| Parameter | In   | Type   | Description          |
| --------- | ---- | ------ | -------------------- |
| `id`      | path | string | UUID of the message. |

Response [#response-13]

```json
{
  "id": "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
  "gmail_id": "18f0c2a9b7d4e5f6",
  "uid": 4821,
  "parent_id": "<CA+prev@mail.acme.com>",
  "thread_id": "thread-af83b21",
  "flags": ["\\Seen"],
  "bcc": [],
  "cc": [],
  "date": "2026-06-11T14:22:00Z",
  "from": ["Jane Doe <jane@acme.com>"],
  "in_reply_to": ["<CA+prev@mail.acme.com>"],
  "message_id": "<CA+abc123@mail.acme.com>",
  "ReplyTo": [],
  "to": ["sales@yourco.com"],
  "subject": "Re: Following up on pricing",
  "size": 18422,
  "internal_date": "2026-06-11T14:22:09Z",
  "mod_seq": 90210,
  "body_plain": "Thanks for the details...",
  "body_html": "<p>Thanks for the details...</p>"
}
```

Draft grounding [#draft-grounding]

`POST /unibox/reply/draft`, `GET /unibox/agent-drafts` and, under `grounding.brain`, `POST /unibox/compose/draft` carry a `grounding` object when a campaign or [campaign brain](/guides/campaign-brain/) applied. It names what the draft read and never holds the text:

```json
{
  "v": 1,
  "surface": "reply",
  "brains": [{ "id": "…", "name": "Acme offer", "revision": 4, "role": "campaign" }],
  "campaign": { "id": "…", "name": "Spring outreach", "sequence_id": "…" },
  "fields": ["offer", "pricing", "booking_url"],
  "voice_profile": false,
  "sources": [{ "kind": "url", "id": "…", "title": "Pricing page", "chars": 1180, "visibility": "public", "why": "matched" }],
  "read": 1,
  "left_out": 3,
  "left_out_chars": 14200,
  "chars": 5320,
  "budget_chars": 8000,
  "flags": [{ "kind": "link_not_allowed", "match": "https://example.net/x", "from_inbound": true, "sentence": "Repeats a link from their email. Check it before sending." }]
}
```

`sources` lists up to 40 sources the draft read, playbooks (`kind: "skill"`) included. `read` and `left_out` count the brain's material only: how many sources it read, past that cap too, and how many it left out. Drafts written before `read` existed leave it out.

`flags[].kind` is one of `link_not_allowed`, `address_not_allowed`, `phone_not_allowed`, `unverified_figure`, `internal_figure`, `internal_phrase` and `do_not_say`; `match` is copied from the draft body. A caller without `manage_campaigns` sees `unverified_figure` in place of `internal_figure` and no `internal_phrase`.

`POST /unibox/agent-drafts/:id/approve` accepts `acknowledge_flags`. An API key or OAuth caller approving a draft with flags gets `409 draft_flagged`, with the `flags`, until it sends `"acknowledge_flags": true`. The draft stays pending. The `flags` are the ones the caller may read: a caller without `WRITE_CAMPAIGNS` gets `unverified_figure` in place of `internal_figure` and no `internal_phrase`, as on the list. Send the acknowledged approval with a new `Idempotency-Key`, or none: the same key with a different body answers `409`.

Errors [#errors]

All error responses follow the standard envelope. See [error codes](/api/error-codes/) for the full list.

```json
{
  "error": "forbidden",
  "message": "Unibox requires an active trial or paid subscription",
  "code": "FORBIDDEN",
  "request_id": "req_8f3a1c2b9d"
}
```

Common cases: `403` when the organization lacks unified-inbox access, `400` for a missing `thread_id`, an invalid cursor or limit, or no organization selected, and `400` when marking more than 500 messages seen in one call.
