> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Notifications

## 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](/guides/in-dashboard-notifications).

### 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.**

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/notifications/unread-count" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": { "count": 3 },
  "meta": {
    "request_id": "req_badge_001",
    "timestamp": "2026-08-28T09:00:00Z"
  }
}
```

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

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/notifications/?unread_only=true&severity=warning&limit=5" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": {
    "data": [
      {
        "id": "notif_4f8c1d2a3b4e5f60718293a4b5c6d7e8",
        "user_id": "user_9f3a2b1c4d5e6f708192a0b1c2d3e4",
        "org_id": "org_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
        "kind": "new_signin_from_new_device",
        "severity": "warning",
        "title": "New sign-in from an unrecognized device",
        "body": "Chrome on macOS — first seen 2 minutes ago.",
        "action_url": "/settings/security",
        "read_at": null,
        "created_at": "2026-08-28T08:59:11Z",
        "expires_at": null,
        "source_pillar": "system",
        "category": "security"
      }
    ],
    "pagination": { "cursor": null, "has_more": false }
  },
  "meta": {
    "request_id": "req_list_001",
    "timestamp": "2026-08-28T09:00:05Z"
  }
}
```

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.

```bash cURL theme={null}
curl -X PUT \
  "https://api.orbit.devotel.io/api/v1/notifications/notif_4f8c1d2a3b4e5f60718293a4b5c6d7e8/read" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": {
    "id": "notif_4f8c1d2a3b4e5f60718293a4b5c6d7e8",
    "user_id": "user_9f3a2b1c4d5e6f708192a0b1c2d3e4",
    "org_id": "org_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
    "kind": "new_signin_from_new_device",
    "severity": "warning",
    "title": "New sign-in from an unrecognized device",
    "body": "Chrome on macOS — first seen 2 minutes ago.",
    "action_url": "/settings/security",
    "read_at": "2026-08-28T09:02:40Z",
    "created_at": "2026-08-28T08:59:11Z",
    "expires_at": null,
    "source_pillar": "system",
    "category": "security"
  },
  "meta": {
    "request_id": "req_mark_001",
    "timestamp": "2026-08-28T09:02:40Z"
  }
}
```

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

```bash cURL theme={null}
curl -X DELETE \
  "https://api.orbit.devotel.io/api/v1/notifications/notif_8c2e9d3f4a5b6c7d8e9f0a1b2c3d4e" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": { "deleted": "notif_8c2e9d3f4a5b6c7d8e9f0a1b2c3d4e" },
  "meta": {
    "request_id": "req_dismiss_001",
    "timestamp": "2026-08-28T09:02:45Z"
  }
}
```

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

```bash cURL theme={null}
curl -X POST \
  "https://api.orbit.devotel.io/api/v1/notifications/read-all" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": { "affected": 2 },
  "meta": {
    "request_id": "req_readall_001",
    "timestamp": "2026-08-28T09:02:46Z"
  }
}
```

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:

```typescript Node.js theme={null}
import { Orbit } from "@devotel/sdk-node";

const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY });

// The parse-only path: check the counter, and only pull rows that moved.
const { data: badge } = await orbit.request<{
  data: { count: number };
}>("GET", "/notifications/unread-count");
if (badge.count > 0) {
  const page = await orbit.notifications.list({
    status: "unread",
    limit: 50,
  });
  for (const n of page.data) {
    // One per-row call — the API has no batch ids body (see above).
    await orbit.notifications.markRead(n.id);
  }
  // …or clear the whole current page in one call:
  // await orbit.notifications.markAllRead();
}
```

### 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.**

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/notifications/digest/settings" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": {
    "organization_id": "org_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
    "enabled": true,
    "frequency": "weekly",
    "categories": ["billing", "security"],
    "last_digest_at": "2026-08-21T09:00:00Z",
    "created_at": "2026-08-01T09:04:11Z",
    "updated_at": "2026-08-21T09:00:00Z"
  },
  "meta": {
    "request_id": "req_digread_001",
    "timestamp": "2026-08-28T09:05:00Z"
  }
}
```

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

```bash cURL theme={null}
curl -X PUT \
  "https://api.orbit.devotel.io/api/v1/notifications/digest/settings" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "frequency": "daily", "categories": ["billing", "security", "messaging"] }'
```

```json 200 OK theme={null}
{
  "data": {
    "organization_id": "org_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
    "enabled": true,
    "frequency": "daily",
    "categories": ["billing", "security", "messaging"],
    "last_digest_at": "2026-08-21T09:00:00Z",
    "created_at": "2026-08-01T09:04:11Z",
    "updated_at": "2026-08-28T09:05:12Z"
  },
  "meta": {
    "request_id": "req_digwrite_001",
    "timestamp": "2026-08-28T09:05:12Z"
  }
}
```

**Step 3 — review who's opted in.**

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/notifications/digest/recipients" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": {
    "data": [
      {
        "id": "ndr_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
        "organization_id": "org_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
        "user_id": "user_9f3a2b1c4d5e6f708192a0b1c2d3e4",
        "email": "jordan@example.com",
        "name": "Jordan Lee",
        "role": "admin",
        "opted_in": true,
        "created_at": "2026-08-01T09:04:11Z",
        "updated_at": "2026-08-01T09:04:11Z"
      }
    ]
  },
  "meta": {
    "request_id": "req_digrecip_001",
    "timestamp": "2026-08-28T09:05:20Z"
  }
}
```

`PATCH /digest/recipients/{user_id}` flips one member's flag (owner/admin):

```bash cURL theme={null}
curl -X PATCH \
  "https://api.orbit.devotel.io/api/v1/notifications/digest/recipients/user_9f3a2b1c4d5e6f708192a0b1c2d3e4" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "opted_in": false }'
```

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

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/settings/preferences" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json 200 OK theme={null}
{
  "data": {
    "notifications": {
      "billing": { "email": true },
      "security": { "email": true },
      "messaging": { "email": false }
    },
    "theme": "system"
  },
  "meta": {
    "request_id": "req_prefs_001",
    "timestamp": "2026-08-28T09:08:00Z"
  }
}
```

Write back the whole map (unknown keys are preserved, so other surfaces'
`theme` block survives):

```bash cURL theme={null}
curl -X PUT \
  "https://api.orbit.devotel.io/api/v1/settings/preferences" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "notifications": {
      "billing": { "email": true },
      "security": { "email": true },
      "messaging": { "email": false }
    },
    "theme": "system"
  }'
```

```json 200 OK theme={null}
{
  "data": {
    "notifications": {
      "billing": { "email": true },
      "security": { "email": true },
      "messaging": { "email": false }
    },
    "theme": "system"
  },
  "meta": {
    "request_id": "req_prefs_002",
    "timestamp": "2026-08-28T09:08:04Z"
  }
}
```

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.
