Messages & realtime
Messages are what has been sent to the caller — today, reports. The routes live under
/inbox, after the folder that receives them. When a report is sent, each recipient gets a
frozen, rendered copy in their Inbox, and the sender keeps a record in their Sent
folder. Folders are server-side filters over the caller’s own messages; every route is scoped
to the caller, so no one can read or mutate another mailbox (except admins, through the
explicit read variants below).
Folders
Section titled “Folders”A folder selects a view over the caller’s deliveries. Pass it as the folder query
parameter on listing and counts.
| Folder | Meaning |
|---|---|
inbox |
Received messages, not archived or trashed. The default. |
starred |
Received messages flagged starred. |
sent |
Batches the caller sent (grouped per send). |
archive |
Received messages flagged archived. |
trash |
Received messages flagged trashed. |
The mail item object
Section titled “The mail item object”A mail item serves both a received message and a sent batch; the scope field says which.
Per-scope fields are simply empty for the other scope (received: recipientNames [],
recipientCount 0; sent: readAt null).
| Field | Type | Description |
|---|---|---|
id |
string | Delivery id. For a sent item, a representative row in the batch. |
scope |
string | received or sent. Picks which id flags target. |
batchId |
string | The send batch this delivery belongs to. |
reportContentId |
string | The immutable content version this delivery carried — the key for the version’s discussion. Always present — deleting a report also removes its versions and their deliveries. |
senderName |
string | Display name of the sender. |
senderEmail |
string | Email of the sender. |
senderImageUrl |
string | null | Received: the sender’s avatar URL, or null. |
reportName |
string | Name of the delivered report. |
dateFrom |
string | Report range start YYYY-MM-DD. |
dateTo |
string | Report range end YYYY-MM-DD. |
message |
string | null | Optional note the sender attached. |
readAt |
string | null | Received: ISO 8601 read instant, or null if unread. Sent: always null. |
createdAt |
string | ISO 8601 delivery instant. |
starred |
boolean | The caller’s star flag. |
archived |
boolean | The caller’s archive flag. |
trashed |
boolean | The caller’s trash flag. |
recipientNames |
string[] | Sent only: recipient names for the “To: …” summary. |
recipientImageUrls |
(string | null)[] | Sent only: recipient avatar URLs for the “To: …” summary, aligned with recipientNames. |
recipientCount |
integer | Sent only: number of recipients. |
Opening a single item (GET /inbox/:id) returns the same fields plus renderedMarkdown
(the frozen rendered body); a sent item also carries recipients (the full recipient list,
each with id, name, email, imageUrl).
A recipient can also mint public share links over a received message’s delivered version via
POST/GET /api/v1/inbox/:id/shares — see
Sharing & discussion.
List the mailbox
Section titled “List the mailbox”GET /api/v1/inbox scope: read
Returns the caller’s deliveries for the given folder, newest first.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
folder |
string | no | One of inbox, starred, sent, archive, trash (default inbox). |
limit |
integer | no | Page size, 1–200. Omit to return the full folder. |
offset |
integer | no | Rows to skip (default 0). |
ownerUserId |
string | no | Instance admin only: read another user’s mailbox. |
workspaceId |
string | no | Workspace admin: read deliveries of the workspace’s single-workspace reports across recipients. |
Admin reads are addressed by ?ownerUserId (instance admin reads a user’s mailbox) or
?workspaceId (workspace admin reads a cross-recipient delivery view); otherwise the caller
reads their own mailbox.
Folder counts
Section titled “Folder counts”GET /api/v1/inbox/counts scope: read
Returns per-folder counts for the mailbox sidebar. unread is the Inbox badge count.
Unread count
Section titled “Unread count”GET /api/v1/inbox/unread-count scope: read
Returns just the caller’s unread Inbox count — a cheap poll for a header badge.
Mark all read
Section titled “Mark all read”POST /api/v1/inbox/read-all scope: write
Marks every message in the caller’s Inbox as read. Returns 204 No Content.
Set flags
Section titled “Set flags”PATCH /api/v1/inbox/flags scope: write
Toggles one or more flags on a mailbox target — a received delivery or a sent batch. At
least one flag must be provided. A target the caller does not own returns 404.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scope |
string | yes | received (target a delivery id) or sent (target a batchId). |
targetId |
string | yes | The delivery id or batch id to flag. |
starred |
boolean | no | Set the star flag. |
archived |
boolean | no | Set the archive flag. |
trashed |
boolean | no | Set the trash flag. |
Returns 204 No Content.
Get a message
Section titled “Get a message”GET /api/v1/inbox/:id scope: read
Returns one mailbox item with its frozen renderedMarkdown. The caller’s relationship to
the delivery picks the variant: a recipient gets the received detail, the sender gets the
sent batch detail (with the full recipient list). Instance admins may read any delivery, and
a workspace admin may read it through the source report’s read access (single-workspace
reports only). A message hidden from the caller returns 404.
Mark one read / unread
Section titled “Mark one read / unread”POST /api/v1/inbox/:id/read scope: write
POST /api/v1/inbox/:id/unread scope: write
Set or clear the read state of a single received message. A message not in the caller’s
mailbox returns 404. Both return 204 No Content.
Realtime
Section titled “Realtime”GET /api/v1/realtime session only
A Server-Sent Events
stream of lightweight cache-invalidation signals for the authenticated user. The web app
consumes it with the browser’s EventSource, which sends the session cookie automatically.
A signal names what changed (and where) — never the changed data itself. On receiving one, the client re-fetches the affected queries through the typed API, so authorization and shaping stay on the existing REST path. Each event is a JSON object:
| Field | Type | Description |
|---|---|---|
type |
string | What changed: work-entry, agent-entry, project, report-discussion, or message. |
workspaceId |
string | Set for workspace-scoped signals; absent for user-scoped ones (message spans a user’s workspaces). |
id |
string | The affected entity id when invalidation is per-id (e.g. report-discussion keyed by the content version id). |
Each event arrives as an SSE data: line:
The connection is routed to the caller’s own hub; workspace fan-out is membership-scoped at publish time, so a client only ever receives events addressed to it.