# Notifications API

11 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /notifications/alert-settings

Get the service alert thresholds

How long a service must be down, unhealthy or restarting before service.crashed, service.unhealthy and service.restart_loop are sent, when an incident closes, and when a backup counts as missed. Which channel receives which event is the channel's own event selection.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (SettingsView)
  - `backupGraceMinutes` (integer (int64), required): Extra time a scheduled backup gets after its due time before backup.missed is sent. 0 = automatic: a quarter of the schedule's interval, between 15 minutes and 6 hours.
  - `crashGraceSeconds` (integer (int64), required): A container that stays down (or restarting) this long is announced as crashed. A restart Docker performed after a crash is announced at once.
  - `flapIncidents` (integer (int64), required): Incidents within the flap window that make a flapping service: announced once, then silent until it has been stable for a whole window.
  - `flapWindowMinutes` (integer (int64), required): The window incidents are counted in, and how long a flapping service must be stable before its recovery is sent.
  - `logLines` (integer (int64), required): Runtime log lines attached to a crash or restart-loop message (redacted for the service's secrets). 0 attaches none.
  - `loopRestarts` (integer (int64), required): Docker restarts within the loop window that make a restart loop.
  - `loopWindowMinutes` (integer (int64), required): The window the restarts are counted in.
  - `recoverAfterSeconds` (integer (int64), required): How long a service must look healthy before the incident is closed and service.recovered is sent.
  - `unhealthyGraceSeconds` (integer (int64), required): The health check must keep failing this long before service.unhealthy is sent.
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## PUT /notifications/alert-settings

Set the service alert thresholds

Replaces every threshold. Takes effect within a minute.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): SettingsView
  - `backupGraceMinutes` (integer (int64), required): Extra time a scheduled backup gets after its due time before backup.missed is sent. 0 = automatic: a quarter of the schedule's interval, between 15 minutes and 6 hours.
  - `crashGraceSeconds` (integer (int64), required): A container that stays down (or restarting) this long is announced as crashed. A restart Docker performed after a crash is announced at once.
  - `flapIncidents` (integer (int64), required): Incidents within the flap window that make a flapping service: announced once, then silent until it has been stable for a whole window.
  - `flapWindowMinutes` (integer (int64), required): The window incidents are counted in, and how long a flapping service must be stable before its recovery is sent.
  - `logLines` (integer (int64), required): Runtime log lines attached to a crash or restart-loop message (redacted for the service's secrets). 0 attaches none.
  - `loopRestarts` (integer (int64), required): Docker restarts within the loop window that make a restart loop.
  - `loopWindowMinutes` (integer (int64), required): The window the restarts are counted in.
  - `recoverAfterSeconds` (integer (int64), required): How long a service must look healthy before the incident is closed and service.recovered is sent.
  - `unhealthyGraceSeconds` (integer (int64), required): The health check must keep failing this long before service.unhealthy is sent.

Response 200: OK (SettingsView)
  - `backupGraceMinutes` (integer (int64), required): Extra time a scheduled backup gets after its due time before backup.missed is sent. 0 = automatic: a quarter of the schedule's interval, between 15 minutes and 6 hours.
  - `crashGraceSeconds` (integer (int64), required): A container that stays down (or restarting) this long is announced as crashed. A restart Docker performed after a crash is announced at once.
  - `flapIncidents` (integer (int64), required): Incidents within the flap window that make a flapping service: announced once, then silent until it has been stable for a whole window.
  - `flapWindowMinutes` (integer (int64), required): The window incidents are counted in, and how long a flapping service must be stable before its recovery is sent.
  - `logLines` (integer (int64), required): Runtime log lines attached to a crash or restart-loop message (redacted for the service's secrets). 0 attaches none.
  - `loopRestarts` (integer (int64), required): Docker restarts within the loop window that make a restart loop.
  - `loopWindowMinutes` (integer (int64), required): The window the restarts are counted in.
  - `recoverAfterSeconds` (integer (int64), required): How long a service must look healthy before the incident is closed and service.recovered is sent.
  - `unhealthyGraceSeconds` (integer (int64), required): The health check must keep failing this long before service.unhealthy is sent.
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## GET /notifications/channels

List notification channels

Credentials are never returned; `secrets` holds masked hints of the ones that are set.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (ChannelListOutputBody)
  - `items` (ChannelView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## POST /notifications/channels

Add a notification channel

Kinds: `email` (SMTP: host, port, tls implicit|starttls|none, username, from, to; password in `secrets`), `slack` and `discord` (webhook URL in `secrets.url`), `telegram` (`config.chatId`, bot token in `secrets.botToken`) and `webhook` (URL in `secrets.url`; every request is signed with HMAC-SHA256 in the `X-NebulaCtrl-Signature` header). A webhook's signing secret is generated when omitted and returned once. Not idempotent: the response can carry that secret, so a repeated name answers 409 `channel_conflict` instead of replaying it. Requires the admin scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): CreateChannelInputBody
  - `config` (ChannelConfig)
  - `enabled` (boolean): Default true.
  - `kind` (enum: email | slack | discord | telegram | webhook, required)
  - `name` (string, required)
  - `rule` (ChannelRuleInput)
  - `secrets` (ChannelSecretsInput)

Response 201: Created (CreateChannelOutputBody)
  - `channel` (ChannelView, required)
  - `signingSecret` (string): Webhook channels: the generated signing secret. Shown only in this response; store it now. Rotate it to get a new one.
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict (`channel_conflict`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /notifications/channels/{channelId}

Get a notification channel

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `channelId` (path, string, required)

Response 200: OK (ChannelView)
  - `config` (ChannelConfig, required)
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `id` (string, required)
  - `kind` (enum: email | slack | discord | telegram | webhook, required)
  - `name` (string, required)
  - `rule` (ChannelRule, required)
  - `secrets` (map of string, required): Masked hints of the stored secrets, keyed by name (url, botToken, password, signingSecret). A key is present when the secret is set; its value never reveals it.
  - `stats` (ChannelStats, required)
  - `test` (ChannelTestState, required)
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /notifications/channels/{channelId}

Update a notification channel

Omitted fields stay as they are; empty credentials keep the stored ones. The kind can not change.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `channelId` (path, string, required)

Request body (application/json, required): PatchChannelInputBody
  - `config` (ChannelConfig)
  - `enabled` (boolean)
  - `name` (string)
  - `rule` (ChannelRuleInput)
  - `secrets` (ChannelSecretsInput)

Response 200: OK (ChannelView)
  - `config` (ChannelConfig, required)
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `id` (string, required)
  - `kind` (enum: email | slack | discord | telegram | webhook, required)
  - `name` (string, required)
  - `rule` (ChannelRule, required)
  - `secrets` (map of string, required): Masked hints of the stored secrets, keyed by name (url, botToken, password, signingSecret). A key is present when the secret is set; its value never reveals it.
  - `stats` (ChannelStats, required)
  - `test` (ChannelTestState, required)
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /notifications/channels/{channelId}

Delete a notification channel

Deletes the channel and its delivery log.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `channelId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## GET /notifications/channels/{channelId}/deliveries

List a channel's deliveries

The delivery log, newest first, kept for 30 days: status (`pending`, `retrying`, `delivered`, `failed`), attempts, the last error and the response code. Errors never contain credentials.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `channelId` (path, string, required)
- `status` (query, enum: pending | retrying | delivered | failed): Only deliveries in this status.
- `cursor` (query, string): Opaque cursor from a previous page's nextCursor
- `limit` (query, integer (int64)): Page size (default 50, max 200)

Response 200: OK (ChannelDeliveryPage)
  - `items` (ChannelDelivery[] | null, required)
  - `nextCursor` (string): Present when older deliveries exist
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## POST /notifications/channels/{channelId}/rotate-secret

Rotate a webhook signing secret

Webhook channels only. The new secret is returned once; deliveries are signed with it from now on.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `channelId` (path, string, required)

Response 200: OK (RotateOutputBody)
  - `signingSecret` (string, required): The new signing secret. Shown only in this response.
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## POST /notifications/channels/{channelId}/test

Send a test notification

Sends a test message through the channel right now and returns the delivery that records the result (status `delivered` or `failed`, with the error and response code). The test is not queued, retried, rate limited or deduplicated, and works on a disabled channel. It also updates the channel's `test` state.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `channelId` (path, string, required)

Response 200: OK (ChannelDelivery)
  - `attempts` (integer (int64), required)
  - `body` (string)
  - `channelId` (string, required)
  - `createdAt` (string (date-time), required)
  - `deliveredAt` (string (date-time))
  - `eventType` (string, required)
  - `fields` (MessageField[] | null)
  - `id` (string, required)
  - `isTest` (boolean, required)
  - `lastError` (string, required): Why the last attempt failed; credentials are masked.
  - `maxAttempts` (integer (int64), required)
  - `nextAttemptAt` (string (date-time))
  - `projectId` (string)
  - `responseCode` (integer (int64)): HTTP status or SMTP reply code of the last attempt.
  - `severity` (enum: info | warning | error, required)
  - `status` (enum: pending | retrying | delivered | failed, required)
  - `title` (string, required)
  - `url` (string)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## GET /notifications/event-types

List notification event types

The events a channel can subscribe to, grouped, with their default severity and whether new channels subscribe by default.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (EventTypesOutputBody)
  - `items` (ChannelEventType[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error
