Notification contract¶
Publishing a notification¶
A publisher sends one request per event. NovaBell fans it out to the requested channels.
{
"channel": ["email", "in_app"],
"to": {
"subscriberId": "b6f2c1de-4a55-4a1e-9a3c-2f0e5d7a1c44",
"email": "user@example.com"
},
"scope": "user",
"severity": "info",
"template": "iam/account-created",
"data": { "accountName": "my-account" },
"actionUrl": "/admincenter/accounts/b6f2c1de-4a55-4a1e-9a3c-2f0e5d7a1c44"
}
Field |
Type |
Required |
Meaning |
|---|---|---|---|
|
|
yes |
Any of |
|
|
yes |
The recipient. For |
|
|
when |
Destination address. |
|
|
no — defaults to |
|
|
|
no — defaults to |
Drives severity routing and filtering in the reader. |
|
|
yes |
A template identifier in |
|
|
no |
Variables interpolated into the subject, the in-app title, and the email body. |
|
|
no |
Where the notification leads. A console-relative path is correct here; NovaBell absolutizes it against the console base when it renders the email. |
Delivery outcomes¶
A publish request has three distinct successful outcomes, and they are not interchangeable.
Status |
Body |
Meaning |
|---|---|---|
|
|
Accepted and dispatched to the requested channels. |
|
|
The recipient has muted every notification. |
|
|
The recipient has turned this template’s category off. |
|
|
The request arrived inside the recipient’s quiet-hours window, evaluated in their own time zone. |
A dropped response is a normal result of a user preference, not an error. Do not retry it.
Preferences are consulted only for scope: "user": one person’s mute cannot silence a
tenant-wide notification.
Failures¶
Status |
Meaning |
|---|---|
|
Missing |
|
Missing, invalid or expired token. |
|
The caller is not an authorized publisher, or the authorization decision denied the send. |
|
NovaBell could not render or dispatch the notification. |
|
The authorization service could not be reached. NovaBell fails closed: it denies rather than delivers. |
An invalid severity rejects the whole request rather than silently dropping the filter.
A partially applied filter is worse than a rejected one, because the reader’s severity
routing cannot tell the difference between “no matches” and “the filter was ignored”.
Reading the feed¶
A console client calls these paths with the signed-in human’s bearer token and an
X-Account-Id header carrying the current context. Every route below is available under
both the /api/notifications and /console/notifications prefixes; a browser client uses
the /console form, which the console gateway proxies.
Method and path |
Purpose |
|---|---|
|
Paginated feed, newest first. |
|
Badge count of unread notifications. |
|
Mark one notification read. |
|
Mark every unread notification read. |
|
Hide one notification from the default feed. |
|
Hide every notification from the default feed. |
|
Registered service categories with localized display names. |
Feed query parameters:
Parameter |
Default |
Meaning |
|---|---|---|
|
|
1–100. |
|
|
Rows to skip. |
|
all |
|
|
all |
Comma-separated subset of |
Access is enforced per row: a request returns the caller’s own user-scope notifications plus
the tenant-scope notifications of the Account named in X-Account-Id, and nothing else. A
dismiss or mark-read of a notification the caller does not own returns 404 rather than
403, so probing cannot confirm that a row exists.
Dismiss is a distinct state from read. A dismissed notification is retained but hidden from the default feed; marking one read leaves it in place.
Live updates¶
Notification Center receives new notifications over Server-Sent Events instead of polling.
EventSource cannot set an Authorization header, so the client exchanges its bearer
token for a short-lived stream cookie first:
# 1. Exchange the session token for a stream cookie (204, Set-Cookie).
curl -X POST https://console.flexgalaxy.ai/console/notifications/stream/auth \
-H "Authorization: Bearer ${TOKEN}" \
-H "X-Account-Id: ${ACCOUNT_ID}" \
--cookie-jar stream.cookies
# 2. Open the stream with that cookie.
curl -N https://console.flexgalaxy.ai/console/notifications/stream \
--cookie stream.cookies
The cookie is HttpOnly, Secure, SameSite=Strict, scoped to the stream path only, and
expires after one hour. The stream emits a comment heartbeat every 25 seconds and an
event: notification frame when a notification is created or dismissed for that user.
Re-run the exchange when the cookie expires.