Notifications API
Imposed rule: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
Last updated: 2025-11-25 (Docs Tasks Md.V · DOCS-NOTIFY-40-001)
All protected endpoints require Authorization: Bearer <token> with a validated stellaops:tenant claim. The gateway may forward X-StellaOps-TenantId as signed envelope context, but a matching raw header is not additionally required and is not a data-tenancy source. Responses use the common error envelope (docs/api/overview.md). Paths are rooted at /api/v1/notify.
Channels
POST /channels— create channel. Body matcheschannels.mdschema. Returns201+ channel.GET /channels— list channels (deterministic order: type ASC, id ASC). Supportstypefilter.GET /channels/{id}— fetch single channel.DELETE /channels/{id}— soft-delete; fails if referenced by active rules unlessforce=truequery.
Rules
POST /rules— create/update rule; idempotency viaIdempotency-Key.GET /rules— list rules with paging (page_token,page_size). Sorted bynameASC.POST /rules:preview— dry-run rule against sample event; returns matched actions and rendered templates.
Policies & escalations
POST /policies/escalations— create escalation policy (seeescalations.md).GET /policies/escalations— list policies.
Deliveries & digests
POST /deliveries— idempotently records worker delivery state. Optional top-levelcveIdandfixtureIdfields are persisted with the delivery payload and echoed on readback.GET /deliveries— query delivery ledger; filters:status,channelId,eventType,since,until,cveId,correlationId,fixtureId,limit,offset.cveIdmust useCVE-YYYY-NNNNformat and be at most 64 characters. ThecveIdandfixtureIdfilters match the persisted delivery payload fields;correlationIdmatches the durable correlation column or persisted payload correlation/trace id. Results are sorted bycreatedAtDESC thenidASC.DELETE /deliveries— tenant-scoped cleanup for delivery ledger rows. Requiresnotify.operator; at least one cleanup filter is required:fixtureId,correlationId, orcveId. Optionaltenantquery must match the authenticated tenant. Response shape:{ "status": "notify-deliveries-cleanup-completed", "deleted": 0, "tenant": "...", "fixtureId": "...", "correlationId": "...", "cveId": "..." }.GET /deliveries/{id}— single delivery with rendered payload hash and attempts.POST /digests/preview— preview digest rendering for a tenant/rule set; returns deterministic body/hash without sending.- Digest schedule cadence is worker configuration (
Notifier:DigestSchedule), not an API-managed resource; current digest endpoints administer rendered output or open windows only.
Acknowledgements
POST /acks/{token}— acknowledge an escalation token. Validates DSSE signature, token expiry, and tenant. Returns200with cancellation summary.
In-app inbox
GET /api/v2/notify/inbox?limit=50returns{ items, unreadCount, nextCursor }for the authenticated tenant and GUID subject.userIdis optional and is an administrator-only override: a different GUID requiresnotify.admin; a viewer receives RFC-7807403. Empty and populated users both return200; missing bearer authentication returns401.- If durable inbox storage times out or reports a database failure, the list returns sealed RFC-7807
503plusRetry-After: 5. The response does not include connection strings, credentials, repository exception text, or a false empty list. The Console propagates that typed failure, retains any last successful snapshot, and renders an accessible retryable error; it never translates an authenticated 503 into zero unread groups or an empty state. - The remaining inbox routes are
GET /api/v2/notify/inbox/unread-count,GET /api/v2/notify/inbox/{messageId},POST /api/v2/notify/inbox/{messageId}/read,POST /api/v2/notify/inbox/read-all, andDELETE /api/v2/notify/inbox/{messageId}. Personal message-id routes are owner-bound; absent and foreign IDs both return the same404. Cross-user list, unread-count, and read-all operations requirenotify.admin. - Alert grouping / bounded recurrence. Repeated alerts of the same
(event kind, typed resource subject)are collapsed into one active row inside a 15-minute recurrence window. Each item carriesoccurrenceCount(source occurrences in that bounded incident group),lastSeenAt, andgroupKey;unreadCountcounts distinct unread alert groups rather than raw occurrences. The stable key isv2:{kind}:sha256:{digest}using central crypto with fixed SHA-256. Only typedcomponent/repo/image/namespace/scope.digest/resourceidentity can group; rendered copy and action digests are excluded. Immediate delivery retries reuselastOccurrenceIdand do not inflate the count. A read group stays read while occurrences continue inside the window; a recurrence after the window archives it and creates a fresh unread group. Seedocs/modules/notifier/architecture.md; embedded migration003_sprint20260717_notifier_inbox_grouping.sqlis immutable legacy history and forward-only004_sprint20260720_secure_bounded_inbox_grouping.sqlperforms secure v2 repair/convergence.
Simulations
POST /simulations/rules— simulate a rule set for a supplied event payload; no side effects. Returns matched actions and throttling outcome.
Health & metadata
GET /health— liveness/readiness probes.GET /metadata— returns supported channel types, max payload sizes, and server version.
Assurance QA fixture endpoints
POST /api/v1/qa/fixtures/advanced-assurance-golden/notify-import— testing-only local import of the golden seed manifest body. Writes Notify-owned channel, queued delivery, and NIS2/DORA/CRA incident-reporting timeline rows. Does not accept filesystem paths and does not claim channel delivery, regulator submission, live publication, Findings ledger rows, or legal compliance.GET /api/v1/qa/fixtures/advanced-assurance-golden/timeline-readiness?evidenceRef=...— testing-only local proof that a golden evidence ref is present in Notify-owned NIS2/DORA/CRA timelines. The response separatesevidenceTimelineReadyfromoperatorHandoffReadyandautoSubmitReady; no legal or publication claim is made.GET /api/v1/regulatory/reporting-timelines?regime=nis2|dora|cra&evidenceRef=...— source-owned timeline query used by the QA fixture proof. It returns persisted Notify timeline state and evidence refs only.
NIS2 Article-23 incident write path
Operator-driven write endpoints that advance the shared incident reporting timeline state machine (OPEN → EARLY_WARNING_SENT → NOTIFICATION_SENT → FINAL_REPORT_SENT → CLOSED) and emit the matching nis2.incident.* Findings-ledger events. The endpoints do not fork the state machine; they validate operator input, invoke the handoff service, and return the same timeline snapshot shape the GET endpoint emits. Illegal transitions fail closed with a stable reason code (409/422), never a silent no-op. Read is notify.viewer; all writes require notify.operator. Tenant is resolved from the validated principal (the gateway injects the trusted X-StellaOps-TenantId); a missing/unresolved tenant returns 400 tenant_missing.
POST /api/v1/regulatory/nis2/incidents— open a timeline. Body requiresincidentId. Returns 201 with the snapshot,ledgerRef, andledgerStatus. Starts the 24h/72h/1-month cadence and emitsnis2.incident.opened.POST /api/v1/regulatory/nis2/incidents/{incidentId}/classify— record the Article-23 significance classification; emitsnis2.incident.classified. Body requiresseverity. The ledger gate for the classified event additionally requires reachability correlation (correlation.reachability[].subgraphRef) AND VEX consensus correlation (correlation.vexConsensus[].{consensusState,statementRef}). Without both, the ledger returnsValidationFailedand the endpoint responds 409nis2_incident_ledger_handoff_failed— this is a correlation-completeness requirement, not an auth/tenant failure. The console classify form collects these correlation fields so a UI-driven classify returns 200.POST /api/v1/regulatory/nis2/incidents/{incidentId}/report— record a milestone transmission (milestone=early_warning|notification|final_report); emitsnis2.incident.reported. Body requiresmilestone,payloadHash,signer,authorityReference.payloadHashmust be a SHA-256 digest assha256:<64hex>(a bare 64-hex value is rejected withreport_payload_hash_invalid). TheauthorityReferenceis echoed onto the recorded transmission as the CSIRT/ENISA deliverymodewith statusRECORDED.POST /api/v1/regulatory/nis2/incidents/{incidentId}/close— close the timeline; emitsnis2.incident.closed. Body requiresclosedByandreason.
Determinism notes
- All list endpoints are stable and include
next_page_tokenwhen applicable. - Templates render with fixed locale
en-USunlessAccept-Languageprovided; rendering is pure (no network calls). bodyHashuses SHA-256 over canonical JSON; repeated sends with identical inputs produce identical hashes and are de-duplicated.
