Skip to content

Crit Alarm API (v1)

The /v1/ routes provide management capabilities ntfy does not have: topic configuration, secret token provisioning, incident lifecycle tracking, two-stage acknowledgment, and server health diagnostics.

All /v1/ endpoints require a management bearer token in the Authorization header:

Authorization: Bearer <management-token>

The required token format depends on the server operational mode:

Mode Credential Format Scope
selfhosted Admin token (ad_...) Grants full access to the server. Single operator, single tenant.
relay, hosted Device token (dv_...) Strictly scoped to the account that owns the registering device.
  • Strict Mode Enforcement: In selfhosted mode, device tokens (dv_...) are rejected. In relay and hosted modes, admin tokens (ad_...) are rejected so that no single shared credential exposes multi-tenant data.
  • Enumeration Prevention: In multi-tenant modes, requests targeting resources belonging to another account return 404 Not Found rather than 403 Forbidden, preventing malicious actors from probing whether topic names exist.
  • Admin Token Lifecycle: On a self-hosted instance, the admin token is generated on initial startup, logged once, and stored in the database. You can display it via critalarm token show or rotate it with critalarm token rotate.

Topics isolate alert feeds. Each topic maintains its own incident state and secret publish tokens.

GET /v1/topics

Response (200 OK):

[
{
"name": "prod",
"critical": true,
"repeat_interval_s": 30,
"max_ring_s": 1800,
"desk_timer_s": 600,
"relay_content": "none",
"created_at": 1757462400
}
]
POST /v1/topics
Content-Type: application/json
{
"name": "prod"
}

Response (201 Created):

{
"name": "prod",
"critical": false,
"repeat_interval_s": 30,
"max_ring_s": 1800,
"desk_timer_s": 600,
"relay_content": "none",
"created_at": 1757462400,
"token": "tk_9q8w7e6r5t4y3u2i1o"
}

Important: The publish token is returned exactly once upon topic creation. Store it securely in your monitoring tools.

  • critical always defaults to false on creation. Do not change this default.
  • relay_content is server-wide configuration and is read-only in this API.
PATCH /v1/topics/prod
Content-Type: application/json
{
"critical": true,
"repeat_interval_s": 45,
"max_ring_s": 3600,
"desk_timer_s": 900
}

Response (200 OK): Returns updated topic object.

DELETE /v1/topics/prod

Response (204 No Content). Deletes the topic and all associated tokens, incidents, and messages.

POST /v1/topics/prod/tokens

Response (201 Created):

{
"token": "tk_a1b2c3d4e5f6g7h8i9"
}
DELETE /v1/topics/prod/tokens/{token_id}

Response (204 No Content).


Incidents represent active or historical alerting events. An incident is opened whenever a priority 5 message is published to a topic with critical: true.

GET /v1/incidents?limit=20&state=open&topic=prod
  • Query Parameters:
    • limit: Number of incidents to return (default: 20, max: 100).
    • state: Filter by state (open, acked, closed, expired).
    • topic: Filter by topic name.

Response (200 OK):

[
{
"id": "inc_9a8b7c",
"topic": "prod",
"state": "open",
"opened_at": 1757462400,
"acked_at": null,
"closed_at": null,
"last_message_at": 1757462400,
"messages": [
{
"id": "m_7f3k2p9q",
"time": 1757462400,
"title": "Uptime Kuma",
"message": "db01 is down",
"priority": 5,
"tags": ["warning"]
}
]
}
]
GET /v1/incidents/{id}

Response (200 OK): Full incident object. In relay-content: none mode, this endpoint is called by the iOS Notification Service Extension (NSE) to retrieve the alert title and body before displaying the critical notification.

Acknowledging stops the active siren loop.

POST /v1/incidents/{id}/ack
  • Response (200 OK):
    {
    "id": "inc_9a8b7c",
    "state": "acked",
    "acked_at": 1757462430,
    "desk_timer_fires_at": 1757463030
    }
  • Error (409 Conflict): Returned if the incident is not in the open state.

Closing marks the incident resolved once you are at your workstation.

POST /v1/incidents/{id}/close
  • Response (200 OK):
    {
    "id": "inc_9a8b7c",
    "state": "closed",
    "closed_at": 1757462700
    }
  • Error (409 Conflict): Returned if the incident is not in the acked state.

open ----(ack)----> acked ----(close)----> closed
| |
| +---- desk_timer expired (reopen)
|
+---- max_ring expired ----> expired
  1. Flapping Guard: Only one active incident exists per topic at a time. If new priority 5 messages arrive while an incident is open or acked, they are appended to the existing incident and update last_message_at without resetting the acknowledgment or creating duplicate alerts.
  2. Repeat Loop: While in the open state, the server re-sends push notifications at the configured repeat_interval_s (default: 30s) using the incident ID as the push collapse key.
  3. Desk Timer Expiry: When an incident is acknowledged (acked), the repeat loop halts and a desk timer begins (default: 10 minutes). If POST /v1/incidents/{id}/close is not called before the desk timer expires, the incident automatically returns to open and restarts the alarm loop.
  4. Max Ring Duration: If an incident remains open without acknowledgment for max_ring_s (default: 30 minutes), the server transitions the incident to expired and ceases repeated pushes.

Send an immediate test alert to verify mobile push delivery and critical sound bypass:

POST /v1/test?topic=prod
  • Response (200 OK):
    {
    "incident_id": "inc_test_12345"
    }
  • Requirements: The specified topic must have critical: true. If the topic is non-critical, the server answers 409 Conflict:
    {
    "error": "topic is not critical"
    }

Unauthenticated diagnostic endpoint called by mobile apps and health monitors to inspect server configuration:

GET /v1/info

Response (200 OK):

{
"name": "critalarm",
"version": "0.1.0",
"base_url": "https://alerts.example.com",
"relay_url": "https://relay.critalarm.app",
"relay_content": "none",
"mode": "selfhosted"
}

The mobile app queries this endpoint when you add a server URL to verify network connectivity, validate semver version compatibility, and read base_url for cryptographic topic hash derivation.