Publish API (ntfy-compatible)
Crit Alarm exposes an ntfy-compatible publish and poll interface at the server root. Any existing tool, library, or script designed for ntfy can publish to Crit Alarm without modification.
Every example here uses the base URL https://alerts.example.com.
1. Publish Endpoint
Section titled “1. Publish Endpoint”POST /{topic}PUT /{topic}- Body: Plain text message body.
- Empty body: Publishes the message text
triggered, matching ntfy behavior. - Topic names: Must match regex pattern
[-_A-Za-z0-9]{1,64}. Any other character or length returns400 Bad Request.
Quick Example with curl
Section titled “Quick Example with curl”curl -X POST https://alerts.example.com/prod \ -H "Authorization: Bearer tk_xxxxxxxxxxxx" \ -H "X-Title: Database Alert" \ -H "X-Priority: 5" \ -H "X-Tags: warning,database" \ -d "db01 replica lag exceeded 300 seconds"2. Authentication
Section titled “2. Authentication”Authentication is required on every publish request. Unlike ntfy, Crit Alarm has no public topics.
You can authenticate using any one of the following methods:
- Bearer token (recommended):
Authorization: Bearer tk_xxxxxxxxxxxx
- Basic auth:
The username portion is ignored; the password must be the topic token.Authorization: Basic base64(anything:tk_xxxxxxxxxxxx)
- Query parameter:
Pass the URL-encoded string or base64 representation ofPOST /prod?auth=Bearer%20dGtf...
Bearer tk_xxxxxxxxxxxx. Use this for tools and webhook senders that cannot set HTTP headers.
The provided token must belong to {topic}. If the token is missing, invalid, or belongs to another topic, the server answers:
{ "code": 40101, "http": 401, "error": "unauthorized"}3. Headers Accepted
Section titled “3. Headers Accepted”Header names are case-insensitive. Every header supports standard ntfy aliases and short query parameter equivalents.
| Header | Aliases | Type | Default | Used For |
|---|---|---|---|---|
X-Title |
Title, ti, t |
string | topic name | Notification title |
X-Priority |
Priority, prio, p |
1 to 5 or name |
3 |
Delivery class; priority 5 opens an incident on critical topics |
X-Tags |
Tags, tag, ta |
comma-separated list | none | Stored and shown in app; emoji shortcodes rendered like ntfy |
X-Click |
Click |
URL | none | URL opened when notification is tapped |
X-Markdown |
Markdown, md |
true, 1, yes |
off | Message body rendered as Markdown in mobile app |
Priority Mapping
Section titled “Priority Mapping”Priority names map to numeric levels as follows:
| Priority Name | Numeric Value | Description |
|---|---|---|
min |
1 | Minimum priority |
low |
2 | Low priority |
default |
3 | Default priority |
high |
4 | High priority (Time-Sensitive push) |
urgent |
5 | Urgent priority (Triggers full incident alarm if topic is critical) |
max |
5 | Equivalent to urgent |
Publish parameters are also accepted via URL query parameters using the same short names:
?t=Notification title?p=Priority (1to5or name)?ta=Comma-separated tags?m=Message body text
4. Headers Accepted and Ignored
Section titled “4. Headers Accepted and Ignored”To prevent breaking existing ntfy webhook integrations, Crit Alarm accepts and ignores the following headers in v1:
X-ActionsX-AttachX-FilenameX-IconX-EmailX-CallX-TemplateX-CacheX-FirebaseX-UnifiedPush
Explicit Rejection of X-Delay
Section titled “Explicit Rejection of X-Delay”X-Delay and its aliases (At, In, or JSON field delay) are explicitly rejected with:
{ "code": 40000, "http": 400, "error": "scheduled delivery not supported"}Crit Alarm never drops a delay and never delivers the message early. Returning success for a schedule it cannot keep is dangerous.
5. JSON Publish
Section titled “5. JSON Publish”Crit Alarm supports publishing messages as JSON payloads to the root endpoint /:
POST /Content-Type: application/jsonAuthorization: Bearer tk_xxxxxxxxxxxx
{ "topic": "prod", "message": "db01 is down", "title": "Uptime Kuma", "priority": 5, "tags": ["warning"], "click": "https://dashboard.example.com", "markdown": false}JSON Schema Fields
Section titled “JSON Schema Fields”topic(string, required): Destination topic name.message(string, optional): Message content. Defaults to"triggered".title(string, optional): Notification title. Defaults to the topic name.priority(integer, optional): Priority level from1to5. Defaults to3.tags(array of strings, optional): List of tags or emoji shortcodes.click(string URL, optional): URL opened on tap.markdown(boolean, optional): Whether to parse message content as Markdown.
Unknown fields are ignored. Legacy ntfy fields (actions, attach, email, call) are accepted and ignored, except delay, which returns 400 Bad Request.
6. Response Format
Section titled “6. Response Format”A successful publish returns 200 OK with a JSON payload:
{ "id": "m_7f3k2p9q", "time": 1757462400, "expires": 1757505600, "event": "message", "topic": "prod", "title": "Uptime Kuma", "message": "db01 is down", "priority": 5, "tags": ["warning"], "incident_id": "inc_9a8b7c"}The response fields and order match ntfy’s message object so ntfy client libraries parse responses without modification.
The incident_id field is an additive Crit Alarm extension. It is present only when a new incident was opened or an existing open incident was joined.
7. Behavior by Priority Level
Section titled “7. Behavior by Priority Level”| Priority | Topic Critical Toggle | Result |
|---|---|---|
| 5 | ON | Incident opened, or joined if one is open. Database repeat loop begins. incident_id returned in response. Phone sounds emergency alarm. |
| 5 | OFF | Message stored in database. Forwarded to push relay as Time-Sensitive / high priority. No incident is opened and no repeat loop runs. |
| 4 | ON or OFF | Message stored. Forwarded to push relay as Time-Sensitive / high priority notification. |
| 1 to 3 | ON or OFF | Message stored in local database. App polls for history. Message is not forwarded to push relay. |
8. Error Responses
Section titled “8. Error Responses”Error responses follow ntfy JSON conventions:
| HTTP Status | Error Code | Error Message | Reason |
|---|---|---|---|
400 |
40001 | invalid topic name |
Topic name contains invalid characters or exceeds 64 characters. |
400 |
40000 | scheduled delivery not supported |
X-Delay header or delay field was specified. |
401 |
40101 | unauthorized |
Missing, invalid, or mismatched topic token. |
413 |
41301 | message too large |
Request body exceeds 4096 bytes. |
429 |
42901 | rate limited |
Ingress publish rate limit exceeded. |
9. Polling Endpoint
Section titled “9. Polling Endpoint”To retrieve message history and sync offline state, clients query the polling endpoint:
GET /{topic}/json?poll=1&since=12hAuthorization: Bearer tk_xxxxxxxxxxxx- Query Parameters:
poll=1(required): Enables one-shot polling mode.since(optional): Filter messages since a message ID, a Unix timestamp (seconds), or a relative duration like10m,1h, orall. Defaults to the last 12 hours.
- Output: Newline-delimited JSON (NDJSON), returning one message object per line (matching the format in section 6), ordered from oldest to newest.
Streaming subscriptions (/json without poll=1, /sse, /ws, and /raw) are not supported in v1 and return 501 Not Implemented.