Skip to content

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).

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.

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.

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.

curl "https://<your-instance>/api/v1/inbox?folder=inbox&limit=50" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

GET /api/v1/inbox/counts scope: read

Returns per-folder counts for the mailbox sidebar. unread is the Inbox badge count.

curl "https://<your-instance>/api/v1/inbox/counts" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

GET /api/v1/inbox/unread-count scope: read

Returns just the caller’s unread Inbox count — a cheap poll for a header badge.

{ "count": 3 }

POST /api/v1/inbox/read-all scope: write

Marks every message in the caller’s Inbox as read. Returns 204 No Content.

curl -X POST "https://<your-instance>/api/v1/inbox/read-all" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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.
curl -X PATCH "https://<your-instance>/api/v1/inbox/flags" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "received", "targetId": "msg_01", "starred": true }'

Returns 204 No Content.

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.

curl "https://<your-instance>/api/v1/inbox/msg_01" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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.

curl -X POST "https://<your-instance>/api/v1/inbox/msg_01/read" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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:

data: {"type":"work-entry","workspaceId":"wrk_demo"}

data: {"type":"message"}

data: {"type":"report-discussion","id":"rct_weekly_v1"}

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.