Inbox
Persistent per-user notifications: messages sent on admin-defined channels land in each recipient's inbox and stay there - with read state, priority, thread grouping, interactive actions, receipts, and delivery controls (scheduled delivery, expiry, self-destruct) - until acted on, expired, or self-destructed.
When to reach for it: in-app notification centers, agent-to-human handoff messages, approval prompts, job status updates - anything a user must be able to find later and mark as handled.
When not to: ephemeral realtime fan-out with no persistence or read state belongs to Pub/Sub; service-to-service work dispatch belongs to Message Queue; an append-only activity feed belongs to Timeline. Email is out of scope entirely.
Concepts
- Channels are named categories (
docs4.alerts) created by a platform tenant administrator or live service principal, with creation authority in its workspace. A channel carries policy that every message on it inherits: default priority, who may send, whether actions and scheduled delivery are allowed (aretention_daysvalue is stored and echoed but nothing sweeps on it; expiry is per message). See Channels and delivery. - Fan-out at send - one send to N recipients creates N independent
messages, each with its own
message_idand its own read state. - Read state is per message - read/unread, dismissed, archived - and aggregate unread counts come broken down by channel, priority, and thread.
- Threads group related messages under a caller-chosen
thread_id(a job id, a conversation id) and get their own summary endpoint. - Actions are buttons attached to a message (
approve,link,retry, ...). Executing one records an auditable result; the server never calls the action'stargetitself - kinds and targets are hints for the client rendering the inbox. - Priority is one of
low,normal,high,urgent; omitted on send, it falls back to the channel's default (verified: a plain send on a default channel arrived asnormal).
Sending
Sending requires channel editor/manager or workspace creation authority. An
empty producer allowlist adds no further restriction; a nonempty allowlist
rejects other producers with 403 producer_not_allowed, except channel managers.
A JWT role string alone does not grant channel authority.
snug inbox send --to user_123 --channel 0199e100-0000-7000-8000-00000000000b \
--title "Deploy finished" --msg "Build 421 is live"
snug inbox send --to user_123 --to user_456 --channel 0199e100-0000-7000-8000-00000000000b \
--msg "Ready" --priority high --thread deploy-421
snug inbox batch-send --file messages.json # JSON array of send requestsRe-sends are made safe with an idempotency key, scoped per channel and
recipient - verified: four sends with --idempotency-key docs4-idem-1
all returned the same message_id, and exactly one message existed.
Over HTTP, POST /api/v1/inbox/messages returns 202 with the created
ids (captured live; actions attached here because the CLI send command
does not take actions):
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "0199e100-0000-7000-8000-00000000000b",
"recipient_user_ids": ["docs-wave"],
"title": "Approve rollout", "body": "Promote canary to 100%?",
"priority": "high",
"actions": [{"id": "approve", "label": "Promote", "kind": "approve"},
{"id": "docs", "label": "Runbook", "kind": "link",
"target": "https://example.com/runbook"}]}' \
http://localhost:4000/api/v1/inbox/messages{
"status": 202,
"msg": "Accepted",
"data": {
"message_ids": ["RqehZAdRfEmNDmnjUSHR"],
"status": "delivered",
"scheduled": false
}
}Reading and read state
snug inbox list --unread
snug inbox list --filter 'channel_id[eq]=0199e100-0000-7000-8000-00000000000b' --filter 'priority[eq]=high' --limit 20
snug inbox get --message-id RqehZAdRfEmNDmnjUSHR --mark-read
snug inbox read --message-id RqehZAdRfEmNDmnjUSHR # returns unread_total
snug inbox unread --message-id RqehZAdRfEmNDmnjUSHR
snug inbox countscounts returns the total plus by_channel, by_priority, and
by_thread maps. Messages, counts, thread summaries, receipts and action results
belong to the recipient. Other users, including channel and workspace managers,
receive 404 for that private message. Administration and producer grants do not
permit impersonating a recipient. Live membership and enabled principal state are
required; token claims alone are insufficient.
list speaks the standard list contract,
newest first, 50 per page by default: q over title and body, and filters
on channel_id, thread_id, priority, lifecycle, read, dismissed,
archived, created_at, and updated_at (e.g. filter[lifecycle][eq]=read).
It only ever lists the caller's own delivered messages; --unread
(unread=true) keeps the unread ones. Archived and
dismissed messages are left out unless --include-archived /
--include-dismissed is passed or the request filters on archived or
dismissed itself.
Threads
snug inbox threads
snug inbox threads --filter 'unread_count[gt]=0'Each summary carries message_count, unread_count,
highest_priority, and the latest message's id and title - verified with
a six-message docs4-job-88 thread. Threads are a
list, newest activity first; they filter
on channel_id, highest_priority, unread_count, message_count, and
latest_at, and sort by unread_count or message_count.
Dismiss, archive, bulk
Dismissed and archived messages keep their data but drop out of default
lists until requested back with --include-dismissed /
--include-archived (verified: list total went 10 -> 8 and back).
snug inbox dismiss --message-id msg_id # dismiss
snug inbox dismiss --message-id msg_id --archive # archive instead
snug inbox bulk --operation mark_read --unread-only --channel 0199e100-0000-7000-8000-00000000000b
snug inbox bulk --operation delete --message-id id1 --message-id id2Bulk operations are mark_read, mark_unread, dismiss, archive,
delete, selected by explicit ids or by filter flags, and return
affected plus the new unread_total.
Actions and receipts
snug inbox action --message-id RqehZAdRfEmNDmnjUSHR --action-id approve \
--idempotency-key act-1
snug inbox receipt --message-id msg_id --kind read --device "ios-app"Executing an action records a completed MessageActionResult (default
result {"acknowledged": true}, or the payload you pass); repeating with
the same idempotency key returns the same result_id instead of a new
record. An unknown action id is 404 action_not_found. A read receipt
also marks the message read (verified) - delivered receipts only append
to the receipt log.
Channels and delivery controls
Channel creation (current tenant administrator or live service principal, with workspace creation access), producer allowlists, required technical notes, scheduled delivery, expiry, and self-destruct are on their own page: Channels and delivery.
Limits and configuration
Message bodies up to 32 KiB, 1000 recipients per send, 8 actions per
message, 500 items per bulk operation, and 100 messages per batch-send by
default - all tunable via the INBOX_* variables.
Reference
- Inbox API - every endpoint, callable
- Related: Pub/Sub for ephemeral fan-out, Timeline for activity feeds, Message Queue for service-to-service work
Identifiers
Channel, message, receipt and action-result IDs are opaque UUIDv7 strings. Thread names, action names and idempotency keys remain business strings.
