WarmblyDocs
Endpoint reference

Unified inbox

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

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

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.

ParameterInTypeDescription
cursorquerystringOpaque pagination cursor from a previous response.
limitqueryintegerPage size. Clamped to the server's min/max.
fromquerystringFilter by sender address (substring).
folderquerystringOne 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.
subjectquerystringFilter by subject (substring).
unseenquerybooleantrue returns only threads with unread messages.
awaiting_replyquerybooleantrue returns only threads where the latest message was sent by you (recipient has not replied).
snoozedquerystringtrue returns only snoozed threads. Omit to exclude snoozed threads (default).
sincequerystringLower bound on date, YYYY-MM-DD.
untilquerystringUpper bound on date, YYYY-MM-DD.
email_idquerystringRestrict to a single mailbox by UUID.
email_idsquerystringComma-separated mailbox UUIDs. A thread matches if it landed in any of them. Invalid UUIDs are dropped.
category_idsquerystringComma-separated conversation-label UUIDs. A thread matches if it carries any of them.
uncategorizedquerybooleantrue returns only threads carrying no conversation labels.
automatedquerybooleantrue returns only conversations no person wrote in: security alerts, notifications, bounces and auto-replies, as judged by automatic 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.

Response

{
  "data": [
    {
      "id": "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
      "email_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "thread_id": "thread-af83b21",
      "from_addr": ["Jane Doe <[email protected]>"],
      "to_addr": ["[email protected]"],
      "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" }
      ]
    }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6Ii4uLiJ9",
    "has_more": true
  }
}

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.

ParameterInTypeDescription
email_idquerystringOptional mailbox UUID to count unread for a single mailbox.

Response

{
  "count": 37
}

Get inbox overview

GET /unibox/overview

Rolls up the scope rail and top metric strip in one call: unread, today, week, snoozed, awaiting-reply, 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

{
  "total": 1284,
  "unread": 37,
  "today": 12,
  "week": 88,
  "snoozed": 3,
  "awaiting_reply": 9,
  "automated": 41,
  "automated_unread": 6,
  "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": "[email protected]",
      "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 /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.

ParameterInTypeDescription
thread_idquerystringRequired. The thread to read. Also accepted as id.
email_idquerystringOptional mailbox UUID to scope the thread to one mailbox. Also accepted as email.
cursorquerystringOpaque pagination cursor.
limitqueryintegerPage 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 does, carries that mailbox's id in answers_mailbox_id; the field is absent otherwise.

Response

{
  "data": [
    {
      "id": "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
      "email_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "mailbox": 1,
      "thread_id": "thread-af83b21",
      "message_id": "<[email protected]>",
      "gmail_id": "18f0c2a9b7d4e5f6",
      "parent_id": "<[email protected]>",
      "uid": 4821,
      "mod_seq": 90210,
      "flags": ["\\Seen"],
      "bcc": [],
      "cc": [],
      "from_addr": ["Jane Doe <[email protected]>"],
      "in_reply_to": ["<[email protected]>"],
      "reply_to": [],
      "to_addr": ["[email protected]"],
      "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 /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.

ParameterInTypeDescription
thread_idquerystringRequired. The thread to read labels for. Also accepted as id.

The response wraps the labels in a data array.

Response

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

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

FieldTypeRequiredDescription
thread_idstringYesThe thread to label.
category_idsstring[]NoThe full desired set of label UUIDs. An empty array clears all labels.
{
  "thread_id": "thread-af83b21",
  "category_ids": [
    "c0ffee00-0000-4000-8000-000000000001",
    "c0ffee00-0000-4000-8000-000000000002"
  ]
}

Response

Returns the resulting label set in a data array.

{
  "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

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 is relayed the same way.

Request body

FieldTypeRequiredDescription
email_idsstring[]NoMessage UUIDs to update (max 500).
thread_idsstring[]NoConversations to update (max 500).
folderstringNoSweep every message in this folder instead of an id list: one of inbox, sent, drafts, archive, spam, trash.
seenbooleanNotrue marks as read, false marks as unread.
{
  "email_ids": [
    "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
    "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
  ],
  "seen": true
}

Response

Echoes the request back.

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

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

FieldTypeRequiredDescription
email_account_idstringYesUUID of the sending mailbox. Any active mailbox in the organization, not only the one holding the thread.
tostring[]YesRecipient addresses (at least one).
ccstring[]NoCC addresses.
bccstring[]NoBCC addresses.
subjectstringYesSubject line.
body_htmlstringNoHTML body.
body_plainstringNoPlain-text body. Leave it empty with body_html set and the plain-text part is rendered from the HTML, link destinations included.
in_reply_tostring[]NoMessage-ID(s) this reply threads under. When omitted, the newest Message-ID in thread_id is used.
thread_idstringNoThe conversation being answered.
send_modestringNoinstant (default), smart (next mailbox gap), or scheduled (use scheduled_at).
scheduled_atstringNoRFC 3339 timestamp. Required when send_mode is scheduled; must be in the future.
forward_message_idstringNoUUID of a stored message (the id from GET /unibox/:id) to forward. See Forwarding a message.
{
  "email_account_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "to": ["[email protected]"],
  "subject": "Re: Following up on pricing",
  "body_html": "<p>Happy to hop on a call this week.</p>",
  "in_reply_to": ["<[email protected]>"],
  "thread_id": "thread-af83b21",
  "send_mode": "instant"
}

Response

{
  "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.

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

Set forward_message_id to forward a message from the inbox. Warmbly 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 Warmbly'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.

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.

{
  "email_account_id": "2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "to": ["[email protected]"],
  "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

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

{
  "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

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

FieldTypeRequiredDescription
thread_idstringYesThe thread to snooze.
snoozed_untilstringYesRFC 3339 timestamp to un-hide the thread.
{
  "thread_id": "thread-af83b21",
  "snoozed_until": "2026-06-12T09:00:00Z"
}

Response

{
  "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

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.

ParameterInTypeDescription
thread_idquerystringRequired. The thread to un-snooze.

Response

204 No Content with an empty body.

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.

ParameterInTypeDescription
thread_idquerystringOptional. 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

{
  "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": "[email protected]",
      "account_name": "Sales",
      "to": ["[email protected]"],
      "subject": "Re: Following up on pricing",
      "snippet": "Happy to hop on a call this week.",
      "thread_id": "thread-af83b21"
    }
  ]
}

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.

ParameterInTypeDescription
task_idpathstringUUID of the scheduled task to cancel.

Response

204 No Content with an empty body.

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.

ParameterInTypeDescription
idpathstringUUID of the message.

Response

{
  "id": "9b6f0e2a-3c4d-4f1a-8b2e-1a2b3c4d5e6f",
  "gmail_id": "18f0c2a9b7d4e5f6",
  "uid": 4821,
  "parent_id": "<[email protected]>",
  "thread_id": "thread-af83b21",
  "flags": ["\\Seen"],
  "bcc": [],
  "cc": [],
  "date": "2026-06-11T14:22:00Z",
  "from": ["Jane Doe <[email protected]>"],
  "in_reply_to": ["<[email protected]>"],
  "message_id": "<[email protected]>",
  "ReplyTo": [],
  "to": ["[email protected]"],
  "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>"
}

Errors

All error responses follow the standard envelope. See error codes for the full list.

{
  "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.

On this page