Skip to main content

Worked sequences

The endpoint list below documents each operation with its parameters. These four sequences cover the read-and-triage loop that powers a notification center of your own: poll the badge, page through the filtered feed, clear it either selectively or in bulk, and set the toggles that decide which categories each member gets notified about. Every request uses your API key (X-API-Key) against https://api.orbit.devotel.io. For the operator-facing breakdown of kinds, delivery channels and the console workflow, see Triage the Notification Center.

Sequence 1 — poll the badge, then read the feed

The loop an external notification center runs on a cadence (the dashboard bell runs it every ~30 seconds per tab): check the counter, and only fetch the filtered list when the counter moved. Step 1 — check the unread counter.
cURL
200 OK
Step 2 — list the filtered feed. unread_only, kind, severity, source_pillar, and category combine on the query string, newest rows first. meta.pagination.has_more tells you whether another page exists; pass meta.pagination.cursor back as cursor and treat it as opaque, keeping the same filters across pages. A malformed filter value is ignored rather than rejected — the list loads unfiltered.
cURL
200 OK
Here has_more is false, so this page is the whole result set — no cursor to pass. Neither this sequence nor the next has any write side effects, so the badge still reports 3 until Step 2 of Sequence 2 runs.

Sequence 2 — clear items, selectively or in bulk

Marking read and dismissing are addressed one row per request — there is no batch ids body on either. When you clear several rows at once (select-all in a notification UI, or a sync loop draining its backlog), loop over the ids and issue the per-row calls in parallel. For “clear everything”, prefer one call to read-all over N per-row calls. Step 1 — mark two rows read. PUT /{id}/read is idempotent, so a parallel-loop retry past the first success is safe; it returns the updated row with read_at populated.
cURL
200 OK
Step 2 — dismiss the other row. DELETE /{id} hides the row immediately for the caller. It takes a 404 NOT_FOUND when the row is org-wide (user_id: null) rather than addressed to you — those rows age out on their own expires_at clock instead of being dismissible.
cURL
200 OK
Step 3 — bulk-clear the rest. One call flips every remaining unread row to read and returns how many it touched. data.affected counts genuine unread→read transitions here — re-marking already-read rows returns zero.
cURL
200 OK
The Node SDK’s notifications.list / notifications.markRead / notifications.markAllRead / notifications.archive helpers wrap these exact routes. For the routes it has no helper for — the unread counter, the org digest toggles below, the /{id}/read PUT — use the generic orbit.request escape hatch:
Node.js

Sequence 3 — read + tune the org digest toggles

The per-organization email digest is the corporate preference surface: which categories are emailed, on what cadence, and to which members. Owner/admin leaves configure it; members inherit it. Security-critical kinds (sign-in alerts, API-key mints, role changes) ignore mute settings and always mint a bell row. Step 1 — read the current digest settings.
cURL
200 OK
categories: [] (empty array) means every category is emailed, not none. Step 2 — flip the toggles. PUT accepts any one of enabled, frequency (daily or weekly), or categories (from billing, security, messaging, campaigns, agents, system); it returns the full saved record so no follow-up GET is needed.
cURL
200 OK
Step 3 — review who’s opted in.
cURL
200 OK
PATCH /digest/recipients/{user_id} flips one member’s flag (owner/admin):
cURL

Sequence 4 — mute a category for your own seat (user-level)

The org digest above is a corporate preference; the per-seat mute is yours alone. Muting lives on GET/PUT /api/v1/settings/preferences — a namespaced user-preferences blob the dashboard uses for theme, locale and notification toggles, not under /notifications itself. Each category sits under the notifications key; a muted category still mints an in-app bell row (only outbound email/push/webhook delivery is suppressed), and security-critical kinds (sign-in, API-key, role-change events) override the mute entirely.
cURL
200 OK
Write back the whole map (unknown keys are preserved, so other surfaces’ theme block survives):
cURL
200 OK
The per-category toggles a member sets apply to their own outbound delivery only — the org digest loop (Sequence 3) still decides which categories the organization emails at all. Set both to agree: mute a row you never want, and expect the digest to follow.

Worked notification-channel samples

This page covers the second consumption shape of the notification stream. Sequences 1–4 above are the human-facing loop: pull the in-app bell feed, triage it, tune the digest. The samples below treat the same feed as a channel you program against — a service, not an operator, reading rows and applying filters. There are two consumption shapes on this page:
  • The dashboard-bell feed (list/read) — persistent rows you poll with GET /api/v1/notifications/ and clear with the mark-read and dismiss calls.
  • Subscription-based routing to outbound sinks — the per-user category subscription (Settings → notifications), the per-organization digest (/digest/settings), and the SSE stream (/stream) decide which rows leave the bell and land on email, push, or a live socket. Webhook endpoints (POST /api/v1/webhooks, event subscriptions documented under Webhooks) are the code-facing sibling: the same developer-failure kinds (dlq_alert, webhook_endpoint_disabled) that mint bell rows also fan out to your subscribed webhook endpoints. For the payload shape your webhook sink receives, see Consume webhooks; for verifying the X-Devotel-Signature HMAC on delivery, see Verify webhook signatures.

1. List the in-app feed

Read the feed the same way the dashboard bell does — newest first, with the query filters applied server-side. At its smallest the call is GET /api/v1/notifications/?unread_only=true; every other filter appends the same way. Every filter is optional; omitting one widens the feed rather than erroring.
cURL
200 OK
Filter semantics, in short:
  • unread_only=true restricts to rows with read_at: null.
  • kind is the exact event type (the full kind catalog lives in the guide under kinds); severity is one of info | warning | error.
  • source_pillar is the product pillar that minted the row (cpaas, ucaas, ccaas, aiaas, cxaas, cspaas, naas, verify, system); category is the cross-pillar facet (sla_breach, webhook_failure, billing, security, compliance, system, …). Combine a pillar with a category to answer “every SLA breach in CCaaS” style queries.
  • pagination.has_more: true means pass pagination.cursor back as the cursor query parameter on the next call, keeping the same filters. The cursor excludes its own row, so the next page starts strictly after it.

2. Mark read and re-list

Clearing a row is one call per id — PUT /{id}/read returns the updated row with read_at set, and is idempotent so a retry after a timeout is safe.
cURL
200 OK
Re-run the same filtered list and the cleared row no longer matches unread_only=true — drop the flag or set it to false to see the row again:
cURL
To drain the feed without tracking ids, POST /api/v1/notifications/read-all flips every remaining unread row in one call and returns data.affected (see Sequence 2 above).

3. Wire an outbound subscription

There is no per-row subscription object: routing to a sink is a small set of configuration writes. Pick the sink, then enable it — the same filters you pass on the list are the ones the outbound surfaces apply for you. Email ingest per user — mind your own seat’s category subscription so the digest only carries what you act on:
cURL
Organization digest — owner/admin subscribes the whole org to the categories that warrant email, on a daily or weekly cadence:
cURL
Both return the full saved record (200) so no follow-up GET is needed, and re-issuing the same write is idempotent — there is no duplicate-subscription conflict to handle. Live push channel — for near-real-time consumption without polling, mint a one-time token and open /stream; the rows that arrive match the same kind / severity / source_pillar / category facets you filter on the list:
cURL
When the token store is unavailable, sse-token returns ot: null with a fallback hint — connect with the legacy ?token=<api-key> parameter instead (server-side callers may use it from the start).

4. Errors

A filter value the schema does not know degrades instead of failing. GET / parses its query leniently: an unknown kind or severity, an over-length cursor, or an off-range limit falls back to “no filter” and the list still loads unfiltered — it never answers 422. This protects the bell feed against stale web bundles and SDK callers, and it means you should treat a 422 from this endpoint as a bug report, not an expected case. Strict paths are the writes. A malformed path id on PUT /{id}/read is rejected before the read:
422
A malformed body on the digest writes (an unknown frequency, a categories entry outside the six enumerated values, a missing opted_in on the recipient toggle) is rejected the same way before any row is read:
422
A well-formed id that does not belong to you returns 404. The endpoint never reveals whether the row exists elsewhere:
404
When you consume the stream through webhooks rather than the bell feed, two more failure shapes matter — a dead-letter backlog (dlq_alert) and an endpoint the API auto-disabled after repeated failures (webhook_endpoint_disabled). Both mint a bell row AND a webhook event, so your sink sees the breakage even when nobody is watching the dashboard; the runbook for re-enabling the endpoint is under Webhook events.

Per-operation envelopes

The envelopes below are the concrete shapes for each operation on this page, so every route’s contract is readable without cross-referencing the worked sequences above.

List notifications — GET /api/v1/notifications

GET /api/v1/notifications
Returns rows newest-first: id, kind, severity, title, body, action_url, read_at (null while unread), created_at, expires_at, source_pillar and category, plus the opaque pagination pair.
200 OK

List global — GET /api/v1/notifications/global

GET /api/v1/notifications/global
The cross-pillar feed — the same row shape as GET / plus the since / until time window the query narrows to.
200 OK

List unread-count — GET /api/v1/notifications/unread-count

GET /api/v1/notifications/unread-count
The single integer behind the bell badge.
200 OK

List stream — GET /api/v1/notifications/stream

GET /api/v1/notifications/stream
GET /stream answers text/event-stream; each frame is event: <type> + data: <json> where <json> is the payload below. The deserialized payload carries type (the event name you filter on — the dashboard publishes notification.created) and data, the full row.
200 OK

Sse-token — POST /api/v1/notifications/sse-token

POST /api/v1/notifications/sse-token
Returns a one-time sset_<hex> token and its TTL in seconds. When the token store is unavailable the route answers { ot: null, fallback: "token" } instead — connect with the legacy ?token=<api-key> parameter.
200 OK

Update read — PUT /api/v1/notifications//read

PUT /api/v1/notifications/{id}/read
Returns the updated row with read_at populated; idempotent.
200 OK

Read-all notifications — POST /api/v1/notifications/read-all

POST /api/v1/notifications/read-all
data.affected counts genuine unread→read transitions; re-marking already-read rows returns zero.
200 OK

List settings — GET /api/v1/notifications/digest/settings

GET /api/v1/notifications/digest/settings
The organization digest defaults every member inherits.
200 OK

Update settings — PUT /api/v1/notifications/digest/settings

PUT /api/v1/notifications/digest/settings
Returns the full saved record, so no follow-up GET is needed.
200 OK

List recipients — GET /api/v1/notifications/digest/recipients

GET /api/v1/notifications/digest/recipients
One row per member with their delivery preference — opted_in is the per-seat toggle an owner/admin flips on the PATCH route.
200 OK

Patch recipients — PATCH /api/v1/notifications/digest/recipients/

PATCH /api/v1/notifications/digest/recipients/{user_id}
Returns the updated recipient row.
200 OK

Preview digest — POST /api/v1/notifications/digest/preview

POST /api/v1/notifications/digest/preview
Returns the full payload the scheduler would email: the lookback window, the aggregate counts, the highlight rows, and is_empty — when true the digest subject would read “nothing to report” and no email goes out.
200 OK

Field-level validation detail

The strict write paths above reject a malformed body with 422 VALIDATION_ERROR; the response also carries error.details.issues, an array naming each rejected field, so the caller can fix the exact field rather than resend the same body:
422
Branch on error.details.issues[].field, correct that field, and resend deliberately — retrying the identical body repeats the 422. For the worked end-to-end failure flows these branches plug into, see the error handling guide.