Onebox is a unified inbox that sits across every mailbox a user has purchased through CMR.Normally each mailbox lives in its own provider account — one in Gmail, another in Outlook, each with its own login. Onebox connects them once and then exposes a single set of endpoints for reading, searching, labelling and sending, regardless of which provider is underneath.
What Onebox gives you#
| Capability | Endpoint |
|---|
| Read every thread across all mailboxes | GET /onebox/threads |
| Open one thread with full message bodies | GET /onebox/thread |
| Search across mailboxes | GET /onebox/search |
| Mark read, star, move, label | POST /onebox/action |
| Manage labels | GET POST PUT DELETE /onebox/labels |
| Send and reply | POST /onebox/send |
The threads feed is the important one: it takes no account parameter. Results span every connected mailbox, and each item carries the account it arrived in. That is the whole point of Onebox — one feed instead of one feed per mailbox.
Identifiers#
Onebox uses the same userId as the rest of the component API: the component user ID issued when the user was created, for example K5PF9Y068AMDGENV5BTCNJJZK55F.Internal UUIDs are not accepted and return:{ "status": 404, "message": "User not found for this partner" }
Within a request, account always means the full address of a connected mailbox, such as andrew.lawyer@aurumlawyer.com.
Connecting a mailbox#
Connection is asynchronous. POST /onebox/connect queues a background job per mailbox and returns immediately:{
"status": 200,
"message": "1 mailbox connection(s) queued",
"data": {
"queued": true,
"count": 1,
"mailboxes": [
{
"email": "andrew.lawyer@aurumlawyer.com",
"provider": "GMAIL",
"jobId": "onebox-connect-andrew.lawyer@aurumlawyer.com",
"connectionId": "bec102ab-d0d1-42b5-9d5d-feda42893ef3"
}
]
}
}
A 200 means accepted, not connected. Poll GET /onebox/connected-mailboxes and wait for the mailbox to appear with status: "active" before calling any read endpoint.Why it is not instant#
Before a Gmail mailbox can be read, Onebox may need to enable the required Google APIs on the mailbox's domain. That runs a browser automation against the Google Cloud Console, so the first mailbox on a given domain can take several minutes. Subsequent mailboxes on the same domain are much faster, because the work is already done.If the mailbox has no stored OAuth refresh token yet, the job hands off to the OAuth consent flow instead, and the connection stays pending until consent completes.Only working mailboxes can connect#
A mailbox is connectable only when all of the following hold:a subscription on that domain is ACTIVE or RENEWING
Anything else is rejected:{
"status": 400,
"message": "nope@example.com is not a working mailbox (must be active with an active/renewing subscription on an active domain)",
"data": null
}
Connecting everything at once#
Pass connectAll: true to connect every working mailbox the user owns, and omit email:{ "userId": "K5PF9Y068AMDGENV5BTCNJJZK55F", "connectAll": true }
connectAll must be a real JSON boolean. The string "true" is rejected, deliberately — a stringified value would otherwise be read as truthy and silently connect every mailbox when only one was intended.
Reading mail#
GET /onebox/threads returns a snippet per thread. To read the actual content, take the threadId and call GET /onebox/thread, which returns every message in the thread with its body in both plain and html.Threads are cursor-paginated:"pagination": {
"nextCursor": "eyJ0eXBlIjoic2VhcmNoX2FmdGVyIiwidmFsdWVzIjpbMTc5MDk0MDczODAwMCwiMTg3NzkzNzQ3NjczMjU5NjE5MCJdfQ",
"total": 4
}
Pass nextCursor back as the cursor parameter to fetch the next page. It is absent on the final page.Each thread carries an isWarmUp flag. When true, the message was generated by warmup traffic rather than a real correspondent — filter these out when showing a customer-facing inbox.
Acting on mail#
POST /onebox/action applies an action to specific messages. threadMessages maps each threadId to the message IDs inside it, so you can act on one message or a whole thread:{
"userId": "K5PF9Y068AMDGENV5BTCNJJZK55F",
"account": "andrew.lawyer@aurumlawyer.com",
"threadMessages": {
"1877937258181177440": ["<Gq3K93hRllxHkWlnjYO1wg@notifications.google.com>"]
},
"action": "read"
}
read and unread are confirmed against Gmail. The accepted vocabulary is enforced by the mail backend rather than by CMR, so an unrecognised value returns:{ "status": 400, "message": "action is required and must be a valid email action" }
Labels#
Labels are created in the underlying provider account, so a label created through Onebox is visible directly in Gmail or Outlook too.One inconsistency to be aware of: GET /onebox/labels returns system labels lowercased (inbox, unread, category_updates), while the labels array on a thread returns Gmail's own casing (INBOX, UNREAD, CATEGORY_UPDATES). Compare case-insensitively.Rename and delete both report emailsUpdated, the number of messages re-tagged or stripped. It is 0 when the label exists but is not applied to anything. Deleting a label never deletes messages.
Disconnecting#
DELETE /onebox/disconnect withdraws Onebox access. Unlike connecting, it takes effect immediately.The mailbox itself is unaffected — it keeps running, keeps receiving mail, and keeps its subscription. Only the unified-inbox view is removed. Call POST /onebox/connect again to restore it.
Errors#
| Code | Meaning |
|---|
400 | Mailbox is not connectable, or the action value is not recognised |
401 | Missing or invalid API key |
404 | User does not belong to this partner, or has no connected mailbox yet |
422 | Request failed validation |
The 404 for a user with no connections reads:{
"status": 404,
"message": "No connected Onebox mailbox found for this user. Connect a mailbox first."
}
This is the most common error when integrating: a read endpoint was called before any mailbox finished connecting. Confirm with GET /onebox/connected-mailboxes first. Modified at 2026-10-02 12:24:50