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
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
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 batchids 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
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
data.affected counts genuine
unread→read transitions here — re-marking already-read rows returns zero.
cURL
200 OK
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
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 onGET/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
theme block survives):
cURL
200 OK
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 theX-Devotel-SignatureHMAC 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 isGET /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
unread_only=truerestricts to rows withread_at: null.kindis the exact event type (the full kind catalog lives in the guide under kinds);severityis one ofinfo|warning|error.source_pillaris the product pillar that minted the row (cpaas,ucaas,ccaas,aiaas,cxaas,cspaas,naas,verify,system);categoryis 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: truemeans passpagination.cursorback as thecursorquery 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
unread_only=true — drop the flag or set it to false to see the row again:
cURL
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
cURL
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
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
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
404
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/notificationsnull 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/globalGET / 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-count200 OK
List stream — GET /api/v1/notifications/stream
GET /api/v1/notifications/streamGET /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-tokensset_<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}/readread_at populated; idempotent.
200 OK
Read-all notifications — POST /api/v1/notifications/read-all
POST /api/v1/notifications/read-alldata.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/settings200 OK
Update settings — PUT /api/v1/notifications/digest/settings
PUT /api/v1/notifications/digest/settings200 OK
List recipients — GET /api/v1/notifications/digest/recipients
GET /api/v1/notifications/digest/recipientsopted_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}200 OK
Preview digest — POST /api/v1/notifications/digest/preview
POST /api/v1/notifications/digest/previewis_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 with422 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
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.