Account notifications
A single Server-Sent Events stream for every notification scoped to the calling account. Use it to power a “what’s happening on my account right now” panel without N poll-loops.
The stream is a v0 surface (2026-05-20) and is read-only — it doesn’t replace the durable audit log or the email channel. It’s an additive, low-latency notification path.
Stream notifications
GET /v1/account/me/notifications
Auth: bearer token via Authorization: Bearer <token> header OR
?ds_token=<token> query-string fallback. The browser EventSource
API can’t set custom headers, so the query-string fallback exists for
that case (the same contract as the transcript stream);
the header still wins when both are supplied. Server-side runtimes that
can set headers (Tauri’s invoke bridge, Node’s eventsource package,
etc.) should prefer the header.
The token must carry the broad read scope; account_owner also
satisfies the gate. Resource-granular scopes such as read:sessions,
read:webhooks, or read:audit deliberately do not, because this one
stream mixes cost telemetry, audit, incident, and session events. Treat both
the token and the resulting stream as sensitive account-wide data.
Frame shape
Each event in the stream uses the SSE event: header as a
discriminator and the data: line as a JSON-encoded payload.
event: cost.threshold_alert
data: {"kind":"cost.threshold_alert","accountId":"acc_...","severity":"warn",…}
event: session.errored
data: {"kind":"session.errored","accountId":"acc_...","errorClass":"driver_error",…}
Subscribers can either:
- Use one
EventSource.addEventListener('cost.threshold_alert', …)per kind (recommended — the native router does the dispatch), or - Subscribe via
EventSource.onmessageand switch onpayload.kindfrom the parsed JSON.
A : heartbeat <ISO8601>\n\n comment frame fires every ~25 seconds
to keep load-balancers from closing idle connections. Ignore these.
Event kinds (v0)
cost.threshold_alert
Fires when the account’s operational cost-to-serve estimate crosses an operator threshold (soft or hard, in either direction). This is a unit-economics signal for product operations. It is not a customer spending cap, an invoice event, or an overage trigger, and it does not email, rate-limit, or interrupt the account.
| field | type | notes |
|---|---|---|
kind |
"cost.threshold_alert" |
discriminator |
accountId |
string |
calling account |
severity |
"warn" | "critical" | "resolved" |
resolved = estimate dropped below soft |
billingCycle |
string |
YYYY-MM UTC |
previousState |
"under-soft" | "between-soft-and-hard" | "over-hard" | null |
null on first-ever evaluation |
currentState |
same enum | |
totalCents |
number |
operational estimate for the current cycle |
thresholdSoftCents |
number |
operator soft threshold |
thresholdHardCents |
number |
operator hard threshold |
at |
string |
ISO8601 server publish time |
incident.broadcast
Fires when a public incident is posted, updated, or resolved.
(Live since 2026-07-07 — this kind was previously declared in the
type union with no publisher; the status-page incident lifecycle
now publishes it.) Incident frames are a platform-wide broadcast:
every account with an open stream receives the same incident,
stamped with its own accountId. The frame carries the incident’s
current severity and title — for full detail (affected components,
update history, resolution state), query the
status endpoints.
| field | type | notes |
|---|---|---|
kind |
"incident.broadcast" |
|
accountId |
string |
calling account |
incidentId |
string |
inc_… |
severity |
"minor" | "major" | "outage" |
|
title |
string |
short headline |
at |
string |
ISO8601 server publish time |
audit.high_severity
Selective republish from the audit log for high-severity actions
(e.g. api_key.revoked, account.byok_anthropic_key_set, team.member_removed).
Low-severity events stay in the audit log only — query
GET /v1/account/audit-log for the full ledger.
| field | type | notes |
|---|---|---|
kind |
"audit.high_severity" |
|
accountId |
string |
|
action |
string |
canonical audit action enum |
actorType |
"customer" | "admin" | "system" |
|
targetResourceId |
string | null |
e.g. key_… |
at |
string |
ISO8601 |
session.errored
Fires when a session’s driver hits a terminal error before the customer-initiated destroy.
| field | type | notes |
|---|---|---|
kind |
"session.errored" |
|
accountId |
string |
|
sessionId |
string |
ses_… |
errorClass |
string |
e.g. driver_error, timeout |
at |
string |
ISO8601 |
Reconnect + persistence
The bus is in-memory only — events with no live subscribers are
dropped on the floor. EventSource’s native auto-reconnect (default
3s backoff) is the v0.1 reconnect story; transient drops resume
without any app-level glue.
There is no Last-Event-ID resume: a reconnect resumes the live stream
without replaying missed frames. For the durable trail of any event covered
by audit.high_severity, query GET /v1/account/audit-log.
Quotas + rate-limit
The SSE route permits up to 10 concurrent subscribers per account and
shares the same global rate-limit bucket as every other authenticated
read. An additional connection receives 429 with Retry-After: 30.
One stream per running app instance is normally sufficient because
every subscriber sees every event.
Customer example (TypeScript)
The stream is plain SSE — no SDK helper is needed. In a browser (or
any runtime with EventSource), pass the token via ?ds_token=
since EventSource can’t set headers:
const es = new EventSource(
`https://api.driftstack.dev/v1/account/me/notifications?ds_token=${KEY}`,
);
es.addEventListener('cost.threshold_alert', (e) => {
const event = JSON.parse(e.data);
showToast(`Cost ${event.severity}: $${event.totalCents / 100}`);
});
es.addEventListener('session.errored', (e) => {
const event = JSON.parse(e.data);
showBanner(`Session ${event.sessionId} errored: ${event.errorClass}`);
});
// later, on app teardown:
es.close();
Server-side runtimes that can set headers should send
Authorization: Bearer <token> instead of the query-string token.
Full design notes live at
docs/internal/driftstack-telemetry-event-schema-for-gui-panel.md
in the source repo.