mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-12 00:05:09 +00:00
509 lines
16 KiB
Plaintext
509 lines
16 KiB
Plaintext
---
|
|
title: Unified inbox
|
|
description: 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, and snooze data is org-scoped, not per-user. Two members of the same organization see the same inbox, the same unread badge, and the same threads. Snoozes and conversation labels are attached to the calling user.
|
|
|
|
## 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). |
|
|
| `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 | Lower bound on date, `YYYY-MM-DD`. |
|
|
| `until` | query | string | Upper bound on date, `YYYY-MM-DD`. |
|
|
| `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. |
|
|
|
|
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
|
|
|
|
```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" }
|
|
]
|
|
}
|
|
],
|
|
"pagination": {
|
|
"next_cursor": "eyJpZCI6Ii4uLiJ9",
|
|
"has_more": true
|
|
}
|
|
}
|
|
```
|
|
|
|
## Get unread count
|
|
|
|
`GET /unibox/count`
|
|
|
|
Returns the org-wide unread message count, optionally scoped to one mailbox. 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
|
|
|
|
```json
|
|
{
|
|
"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. Auth: **Scope** `READ_UNIBOX` · **Org permission** `access_unibox`.
|
|
|
|
### Response
|
|
|
|
```json
|
|
{
|
|
"total": 1284,
|
|
"unread": 37,
|
|
"today": 12,
|
|
"week": 88,
|
|
"snoozed": 3,
|
|
"awaiting_reply": 9,
|
|
"scheduled_pending": 2,
|
|
"scheduled_pending_max": 50,
|
|
"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 /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).
|
|
|
|
### Response
|
|
|
|
```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"],
|
|
"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 conversation labels (your categories) attached to a thread. 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
|
|
|
|
```json
|
|
{
|
|
"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. The body's `category_ids` is the desired set, so the call is idempotent and retries are naturally safe. Only your own categories are attached. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.
|
|
|
|
### Request body
|
|
|
|
| Field | Type | Required | Description |
|
|
| --- | --- | --- | --- |
|
|
| `thread_id` | string | Yes | The thread to label. |
|
|
| `category_ids` | string[] | No | The full desired set of category 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
|
|
|
|
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
|
|
|
|
`PATCH /unibox/seen`
|
|
|
|
Marks messages as read or unread, org-wide: either an explicit batch of up to 500 ids, or a whole canonical folder at once. Send one of `email_ids` or `folder`; sending both returns `400`. Auth: **Scope** `WRITE_UNIBOX` · **Org permission** `access_unibox`.
|
|
|
|
### Request body
|
|
|
|
| Field | Type | Required | Description |
|
|
| --- | --- | --- | --- |
|
|
| `email_ids` | string[] | No | Message UUIDs 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
|
|
|
|
Echoes the request back.
|
|
|
|
```json
|
|
{
|
|
"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 from one of your mailboxes. 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
|
|
|
|
| Field | Type | Required | Description |
|
|
| --- | --- | --- | --- |
|
|
| `email_account_id` | string | Yes | UUID of the sending mailbox. |
|
|
| `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. |
|
|
| `in_reply_to` | string[] | No | Message-ID(s) this reply threads under. |
|
|
| `thread_id` | string | No | Thread to thread the reply into. |
|
|
| `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. |
|
|
|
|
```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
|
|
|
|
```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`.
|
|
|
|
## 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
|
|
|
|
```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
|
|
|
|
`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
|
|
|
|
| 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
|
|
|
|
```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
|
|
|
|
`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
|
|
|
|
`204 No Content` with an empty body.
|
|
|
|
## List scheduled sends
|
|
|
|
`GET /unibox/scheduled`
|
|
|
|
Returns the outbound emails you have queued but not yet sent. 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
|
|
|
|
```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
|
|
|
|
`DELETE /unibox/scheduled/:task_id`
|
|
|
|
Cancels a pending scheduled send 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
|
|
|
|
`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`.
|
|
|
|
| Parameter | In | Type | Description |
|
|
| --- | --- | --- | --- |
|
|
| `id` | path | string | UUID of the message. |
|
|
|
|
### Response
|
|
|
|
```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>"
|
|
}
|
|
```
|
|
|
|
## 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.
|