# Advanced usage
You’ve got the basics working. This page covers the knobs that matter once you’re in production.
# Polling, ETags, and “realtime”
Updates are not WebSocket push. The client polls, and the server makes idle polls cheap.
sequenceDiagram
participant App
participant API
App->>API: GET /v1/notifications (If-None-Match: W/"abc")
alt unchanged
API-->>App: 304 Not Modified
else changed items or unreadCount
API-->>App: 200 + new ETag + items + unreadCount
App->>App: update bell UI
end| Behaviour | Detail |
|---|---|
| Default interval | 30 000 ms (SubscribeOptions.intervalMs) |
| Hidden tabs | Polling pauses |
| Tab visible again | Immediate refresh |
| Count-only changes | Included in list ETag — badge stays honest even if the change is off the first page |
Need sub-second delivery someday? The design allows SSE behind the same subscribe() API later — no need to rewrite call sites.
# Pagination and filters
const page1 = await client.list({ limit: 50 });
if (page1.nextCursor) {
const page2 = await client.list({
limit: 50,
cursor: page1.nextCursor,
});
}
await client.list({ unreadOnly: true });
await client.list({ since: '2026-07-01T00:00:00Z' }); // ISO with offset
Notes:
- Keyset cursor (opaque string) — don’t parse it
- Default page size 50, max 200 (server-enforced)
unreadCounton the list response is the total for the user, not “unread on this page”
# Retries and timeouts
| Setting | Default | Meaning |
|---|---|---|
timeoutMs |
10000 |
Per attempt |
maxRetries |
3 |
Extra tries after the first |
retryBaseDelayMs |
250 |
Exponential backoff + full jitter |
| Publish retries | only if dedupeKey set |
Prevents duplicate rows on lost responses |
Reads and mark-read/delete are always safe to retry.
publish / publishSelf without a key fail fast on network/5xx instead of guessing.
401 handling: the client calls getAccessToken again once and retries (for refresh-token flows).
# Metadata
Use metadata for producer context that isn’t title/body:
await client.publish({
userGuid,
title: 'Review complete',
body: 'Feedback is ready.',
dedupeKey: `review_case:${caseId}`,
metadata: {
caseId,
activityInstanceId,
product: 'assess',
},
});
Rules:
- Opaque JSON object
- Max ~8 KB serialized
- Not queried by the service — your UI/app interprets it
- V1 leftover fields land under
metadata.v1when using legacy Create
# Self-publish vs publisher publish
| Self | Publisher | |
|---|---|---|
| Endpoint | /v1/notifications/self |
/v1/notifications |
| Token | Any user | Publisher scope/group |
| Recipient | Always sub |
Any userGuid |
| Typical caller | Browser | Backend job |
| Spoof risk | None (forced to self) | Trusted server only |
# Multiple Cognito pools
AUTH_ISSUER accepts a comma-separated list. The Worker picks JWKS from the token’s iss claim. One pool per deployment is still the common case.
# Retention
A daily cron deletes notifications older than RETENTION_DAYS (default 90). Set 0 to disable.
Plan product copy accordingly (“older than 90 days may disappear”) if users expect permanent history.
# Custom client options
new NotificationsClient({
baseUrl,
getAccessToken,
timeoutMs: 8_000,
maxRetries: 2,
retryBaseDelayMs: 200,
fetch: myFetch, // tests / non-standard runtimes
});
# When to read the OpenAPI
Interactive contract: https://notifications.totumcloud.com/docs
Committed snapshot: packages/api/openapi.json
If the snapshot and the running service disagree, something’s wrong with the deploy — trust CI’s OpenAPI drift check.
Next: Troubleshooting or the Developer reference.