REST API Documentation
Notifly provides a REST API for managing channels, clients, messages, monitoring, metrics, and users.
All examples use the API base URL https://api.notifly.ru (you can substitute it using the $NOTIFLY_URL variable).
https://notifly.ru is the documentation site; there is no API there. The admin UI (web interface) lives at
https://app.notifly.ru/.
Authentication
Section titled “Authentication”Notifly tokens are distinguished by their first letter. Every token is 23 characters long (prefix + 22).
| Type | Prefix | Purpose |
|---|---|---|
| Channel key (app token) | A | Sending to its own channel (POST /message, POST /ask, monitoring receivers); with read permission, also reading that channel’s messages |
| Channel access token | S | The recipient’s key for delegated access: same as A…, but sending only with the send permission |
| API token | M | Programmatic access to the account (read or read+write, scoped by channel), see API tokens |
| Device token | C | An app or browser session: everything an API token can do, plus managing devices, API tokens, password and 2FA. Not a key for integrations |
| Heartbeat ping token | H | Only in the /heartbeat/ping/{token} URL |
| Webhook router token | R | Only in the /router/{token}/… URL |
Web-script tokens (T…) and metric-source tokens (M…, but not an API token) likewise live only in the
URL of their public endpoints and grant no access to the account.
The token is passed in one of these ways (if several are sent, the first one in this list wins):
- the
X-Notifly-Keyheader; - the
X-Gotify-Keyheader (legacy, for compatibility with Gotify clients); - the
Authorization: Bearer …header; - the
?token=query parameter — for transports without headers (WebSocket, SSE,<img>); - the
notifly-client-tokencookie — set byPOST /auth/local/loginfor the browser.
# 1. X-Notifly-Key headercurl -H "X-Notifly-Key: MtVOKLDNuD7HR9y7QkMecC5" https://api.notifly.ru/application
# 2. Query parametercurl "https://api.notifly.ru/application?token=CK2UDmhduKshCyz-RtqxiLx"
# 3. Bearer tokencurl -H "Authorization: Bearer CK2UDmhduKshCyz-RtqxiLx" https://api.notifly.ru/applicationChannel tokens (A…/S… with read permission) are also accepted on some client endpoints, but
see only their own channel: GET /application returns just that channel, GET /message and
GET /message/search return only its messages. Account-level operations (devices, API tokens,
monitors, the user) are forbidden for them — 403.
Basic Auth (username/password) is also supported — but only on the endpoints a client needs before it has a token:
| Endpoint | Purpose |
|---|---|
POST /auth/local/login | exchange username and password for a device token |
GET /current/user | verify the credentials |
GET /client, GET /client/online, POST /client, POST /client/:id/reconnect | create a device or reconnect an existing one |
POST /application | create a channel to get its app token |
POST /web-script/:id/sourcemaps | upload source maps from CI |
Every other endpoint rejects username/password — a token is required. Login
attempts are rate-limited per source address, and an account with an unverified
email is not let in anywhere. An account with 2FA enabled cannot pass with a
Basic pair (401 second factor required) — only via POST /auth/local/login and the second step.
In the examples below, $TOKEN is an API token (M…) from the API tokens
page of the admin UI or a device token (C…) from the POST /auth/local/login response. Where a
device token specifically is required, this is stated explicitly.
Server information
Section titled “Server information”Endpoints are available without authentication.
GET /health — health check
Section titled “GET /health — health check”curl https://api.notifly.ru/health{"health": "green", "database": "green"}If the database is unavailable — 503 with "health": "red".
GET /version — server version
Section titled “GET /version — server version”curl https://api.notifly.ru/version{"version": "ya-1.0.0", "commit": "...", "buildDate": "..."}GET /serverinfo — server flags
Section titled “GET /serverinfo — server flags”curl https://api.notifly.ru/serverinfo{"version": "ya-1.0.0", "register": false, "oidc": false}Authorization (session)
Section titled “Authorization (session)”POST /auth/local/login — login with username/password
Section titled “POST /auth/local/login — login with username/password”The username and password are passed via Basic Auth. Creates a device, returns its token
(the full value appears only in this response) and sets a cookie. Optional body fields:
name (device name) and platform.
curl -u user@example.com:password https://api.notifly.ru/auth/local/login \ -X POST -d "name=my-cli-client"{ "id": 1, "name": "my-cli-client", "token": "CK2UDmhduKshCyz-RtqxiLx", "user_id": 1, "platform": "web"}If the plan’s device limit is reached, the oldest active device is suspended and the response
includes suspended_device_id. If the account has 2FA enabled, instead of a device you get
401 with an mfa field — see Two-factor login.
POST /auth/logout — logout
Section titled “POST /auth/logout — logout”Rotates the device token and clears the cookie. Responds 200 even with an invalid token.
curl -H "X-Notifly-Key: CK2UDmhduKshCyz-RtqxiLx" \ https://api.notifly.ru/auth/logout -X POSTTwo-factor login
Section titled “Two-factor login”For an account with 2FA, a correct password on POST /auth/local/login returns a challenge
instead of a device, with status 401:
{ "error": "Unauthorized", "errorCode": 401, "errorDescription": "second factor required", "mfa": { "challenge": "…", "methods": ["totp", "mobile", "recovery"], "mobileCode": "47", "expiresIn": 300 }}mobile is offered only if sign-in approval from a phone is enabled in the profile.
The challenge lives for 5 minutes and is single-use.
| Method and path | Authentication | Description |
|---|---|---|
POST /auth/mfa/verify | challenge in body | Second step. Body: challenge, method (totp|recovery|mobile), code (for totp/recovery), optional name, platform. Success — the same response as login. Wrong code — 401 invalid code, dead challenge — 410 |
POST /auth/mfa/challenge | challenge in body | Polling for the mobile method: {"status": "pending|approved", "mobileCode", "expiresIn"}; unknown challenge — 404 |
GET /auth/mfa/pending | device token | For the phone: sign-ins awaiting approval (ip, kind, createdAt, expiresIn) |
POST /auth/mfa/approve | device token | For the phone: approve a sign-in, body {"code": "47"} (the code from the login screen). No match — 404, device is not an approver — 403 |
For the mobile method: the web shows mobileCode, the owner enters it in the app
(/auth/mfa/approve), the web polls /auth/mfa/challenge and, once approved, calls
/auth/mfa/verify with "method": "mobile".
curl https://api.notifly.ru/auth/mfa/verify \ -H "Content-Type: application/json" \ -d '{"challenge": "…", "method": "totp", "code": "123456", "name": "my-cli-client"}'POST /user/reset-password — request password reset
Section titled “POST /user/reset-password — request password reset”Public endpoint (no authentication). Sends an email with a password reset link
to the specified address. The response is always 200, even if the user does not
exist — so as not to reveal which addresses are registered.
curl https://api.notifly.ru/user/reset-password \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com"}'POST /user/reset-password/confirm — confirm password reset
Section titled “POST /user/reset-password/confirm — confirm password reset”Public endpoint. Accepts the token from the email and the new password pass (at least
8 characters, no longer than 72 bytes).
curl https://api.notifly.ru/user/reset-password/confirm \ -H "Content-Type: application/json" \ -d '{"token": "<from the email>", "pass": "new-password"}'A password reset is the recovery path after an account takeover, so along with the
password the secrets issued to the account are revoked too: all devices are signed
out (their tokens are rotated; access can be restored by signing in again or via
POST /client/:id/reconnect) and all API tokens are deleted — they have to be
issued again. Channel keys (A…) and channel access tokens (S…) keep working:
they are limited to a single channel and grant no access to the account.
GET /user/verify — email verification
Section titled “GET /user/verify — email verification”Public endpoint. Opened from the link in the verification email
(/user/verify?token=...) and activates the user’s address.
curl "https://api.notifly.ru/user/verify?token=<from the email>"Channels
Section titled “Channels”A channel (application in the API URLs — for compatibility with the Gotify protocol) is a message source. Every channel has its own app token for sending.
GET /application — list of channels
Section titled “GET /application — list of channels”Your own channels and channels you have accepted delegated access to.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application[ { "id": 1, "token": "ApNK7NiI0ltrJf3a8BNsOiD", "name": "Monitoring", "description": "Alerts from the monitoring system", "internal": false, "image": "https://…/channel-logos/1/1/9f2c….png", "defaultPriority": 5, "lastUsed": "2025-01-15T12:00:00Z", "sortKey": "a0", "tags": ["prod"], "hbTotal": 2, "hbStatus": "ok" }]Channel fields:
| Field | Description |
|---|---|
token | The channel’s primary key; for a shared channel — the recipient’s own S… token (empty with view) |
image | Public URL of the logo (empty if none is uploaded) |
sortKey | Manual sort key |
parentId | Parent channel (channel tree); absent for a root channel |
tags | Channel tags |
deliveryConfig | Delivery settings; adapter secrets are masked |
shared, sharePermission, canShare | Only on channels shared with you: the flag, access level (view|send) and the right to reshare |
hbTotal, hbAlerting, hbAlertingInfo, hbPending, hbStatus | Heartbeat summary of the channel; hbStatus — ok|degraded|alerting|pending|none |
POST /application — create a channel
Section titled “POST /application — create a channel”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application \ -H "Content-Type: application/json" \ -d '{"name": "CI/CD", "description": "Build notifications", "defaultPriority": 5}'Request fields (JSON; name, description, defaultPriority, parentId, sortKey can also be sent as a form):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Channel name |
description | string | Description | |
defaultPriority | integer | Default priority — applied to messages sent without priority | |
parentId | integer | Parent channel (channel tree) | |
sortKey | string | Manual sort key | |
tags | string[] | Channel tags (up to 10, [a-z0-9_-]) — used by the scope of API tokens | |
deliveryConfig | object | Re-delivery of unread messages via email/SMS/call/Slack/Telegram/webhook/Discord/Teams/Mattermost/PagerDuty/Opsgenie — see Delivery |
PUT /application/{id} — update a channel
Section titled “PUT /application/{id} — update a channel”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application/1 \ -X PUT -H "Content-Type: application/json" \ -d '{"name": "CI/CD v2", "description": "Updated description"}'DELETE /application/{id} — delete a channel
Section titled “DELETE /application/{id} — delete a channel”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application/1 -X DELETEGET /application/{id}/status — aggregated channel status
Section titled “GET /application/{id}/status — aggregated channel status”Returns the channel’s aggregated status (heartbeat summary, last activity).
With ?recursive=1 — for the channel and all its descendants.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application/1/statusPOST /application/{id}/delivery/test — test delivery to external integrations
Section titled “POST /application/{id}/delivery/test — test delivery to external integrations”Immediately (bypassing escalation) sends a test notification through the channel’s enabled
delivery methods (email, SMS, call, Slack, Telegram, webhook, Discord, Teams, Mattermost,
PagerDuty, Opsgenie) and returns the result for
each. The optional body {"adapter": "slack"} limits the check to a single adapter.
Details — Delivery and escalation.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application/1/delivery/test \ -H "Content-Type: application/json" -d '{"adapter": "telegram"}'{"results": {"telegram": {"ok": true}, "slack": {"ok": false, "error": "..."}}}If no methods are enabled — 400 no enabled adapter to test.
GET /application/{id}/event-sources — event sources of a channel
Section titled “GET /application/{id}/event-sources — event sources of a channel”Everything that writes to the channel: monitors of every type, heartbeats, port scans, metrics,
web scripts, email-inbox and webhook-router rules — in a single list, with the current
status and the time of the last check. Source secrets (tokens, headers, steps) are not
included in the output. With ?recursive=1 — including the channel’s descendants.
Channel logo
Section titled “Channel logo”| Method and path | Description |
|---|---|
POST /application/{id}/image | Upload a channel logo (multipart/form-data, field file) |
DELETE /application/{id}/image | Delete the logo |
Messages
Section titled “Messages”POST /message — send a message
Section titled “POST /message — send a message”The body must be JSON (Content-Type: application/json): this endpoint currently does not accept
form-data or urlencoded bodies (400).
curl "https://api.notifly.ru/message?token=ApNK7NiI0ltrJf3a8BNsOiD" \ -H "Content-Type: application/json" \ -d '{"message": "Build #42 finished", "title": "CI/CD", "priority": 5}'Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
message | string | ✓ | Message text |
title | string | Title | |
priority | integer | Priority (0–10); if omitted — the channel’s defaultPriority | |
extras | object | Additional fields for clients (see msgextras) |
Example with extras (markdown content):
curl "https://api.notifly.ru/message?token=ApNK7NiI0ltrJf3a8BNsOiD" \ -H "Content-Type: application/json" \ -d '{ "message": "**Done!** Details: [link](https://example.com)", "title": "Build", "priority": 5, "extras": { "client::display": {"contentType": "text/markdown"} } }'Response:
{ "id": 123, "appid": 1, "message": "Build #42 finished", "title": "CI/CD", "priority": 5, "date": "2025-06-01T10:30:00Z", "senderName": "user@example.com"}Every message consumes one event of the daily quota; once it is exhausted — 429 daily event quota exceeded.
Writing to the system channel is forbidden — 403.
POST /integrations/alertmanager, POST /integrations/zabbix — receiving from monitoring systems
Section titled “POST /integrations/alertmanager, POST /integrations/zabbix — receiving from monitoring systems”Native alert receivers authorized with the channel’s app token (the
X-Notifly-Key header, Authorization: Bearer or ?token=), just like POST /message.
/integrations/alertmanager— the Prometheus Alertmanager webhook body (version 4) or a Grafana webhook contact point; creates a message for firing alerts and a separate one for resolved alerts./integrations/zabbix— the parameters of the Zabbix “Webhook” media type (subject,message,severity,status, …); one message.
The priority is derived from the source’s severity; each message consumes one quota
event. Response — {"created": N, "ids": [...]}. Setup and mapping —
Alertmanager, Grafana and Zabbix.
GET /message — all messages (with pagination)
Section titled “GET /message — all messages (with pagination)”Messages from all accessible channels (your own and those shared with you), newest first. An API token sees only the channels in its scope, a channel token — only its own channel.
curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/message?limit=20"Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 100 | Number of messages (1–200) |
since | integer | Pagination cursor: return messages with an ID lower than this. Taken from paging.since / paging.next of the previous page |
Response:
{ "paging": { "size": 20, "limit": 20, "since": 4012, "next": "/message?limit=20&since=4012" }, "messages": [ { "id": 4031, "appid": 1, "message": "Message text", "title": "Title", "priority": 5, "date": "2025-06-01T10:30:00Z", "read": true, "senderName": "user@example.com" } ]}paging.since and paging.next are present only when a next page exists; they are absent on the
last page. In a message, read: true means it has been read by the current user (unread messages
have no such field), senderName is the sender, extras — if they were provided.
GET /application/{id}/message — channel messages
Section titled “GET /application/{id}/message — channel messages”Same pagination and response format as GET /message. With ?recursive=1 — messages of the
channel and all its descendants (for a share recipient — only those shared with them).
curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/application/1/message?limit=50"curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/application/1/message?recursive=1"GET /message/search — full-text search
Section titled “GET /message/search — full-text search”Searches the user’s messages. The q parameter is required (at least 2 characters).
The optional appId limits the search to a single channel. Together with appId, the
recursive=1 flag extends the search to the channel and all its descendants (for a share
recipient — only to those shared with them). Supports pagination (limit, since). A channel
token searches only its own channel.
curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/message/search?q=error&limit=20"curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/message/search?q=deploy&appId=1"curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/message/search?q=deploy&appId=1&recursive=1"POST /message/read — mark messages as read
Section titled “POST /message/read — mark messages as read”Body: {"ids": [1, 2, 3]} — a list of message IDs. The owner’s read mark stops
re-delivery; a read-only API token is not allowed here.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/message/read \ -H "Content-Type: application/json" -d '{"ids": [1, 2, 3]}'POST /application/{id}/message/read — mark entire channel as read
Section titled “POST /application/{id}/message/read — mark entire channel as read”With ?recursive=1 — together with its descendants.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application/1/message/read -X POSTDELETE /message — delete all messages
Section titled “DELETE /message — delete all messages”Not available to an API token with a restricted scope.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/message -X DELETEDELETE /message/{id} — delete a message
Section titled “DELETE /message/{id} — delete a message”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/message/123 -X DELETEDELETE /application/{id}/message — delete all messages in a channel
Section titled “DELETE /application/{id}/message — delete all messages in a channel”With ?recursive=1 — together with its descendants.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/application/1/message -X DELETEClients
Section titled “Clients”A client is a device or application that receives messages and manages resources.
GET /client — list of clients
Section titled “GET /client — list of clients”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/client[ { "id": 1, "name": "firefox", "token": "C…xiLx", "lastUsed": "2025-06-01T10:00:00Z", "status": "active", "createdAt": "2025-05-20T08:00:00Z", "platform": "web", "lastIp": "203.0.113.7", "lastUserAgent": "Mozilla/5.0 …", "current": true }]The token in the list is only a hint, not a secret. The full token value is returned once: in the
responses of POST /auth/local/login, POST /client and POST /client/{id}/reconnect. status is
active|suspended|revoked; current: true marks the device the request was made from.
POST /client — create a client
Section titled “POST /client — create a client”Body: name (required), platform. Response — the created client with the full token. When the
plan’s device limit is reached — 402 with errorCode: "device_limit_exceeded".
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/client \ -H "Content-Type: application/json" \ -d '{"name": "my-script"}'PUT /client/{id} — rename a client
Section titled “PUT /client/{id} — rename a client”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/client/1 \ -X PUT -H "Content-Type: application/json" \ -d '{"name": "renamed-client"}'DELETE /client/{id} — delete a client
Section titled “DELETE /client/{id} — delete a client”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/client/1 -X DELETEDELETE /client — delete all devices except the current one
Section titled “DELETE /client — delete all devices except the current one”Response — {"deleted": N}.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/client -X DELETEDevice state and muted channels
Section titled “Device state and muted channels”By default a device receives messages from all channels. The “subscriptions” in the paths below are historical names: they actually hold the list of channels muted on the device.
| Method and path | Description |
|---|---|
GET /client/online | List of device IDs with an active WS connection |
POST /client/{id}/reconnect | Rotate the device token; the new token is returned once |
PUT /client/{id}/status | Change the status, body {"status": "active|suspended|revoked"}; revoked is irreversible, reactivating over the limit — 402 |
GET /client/{id}/activity | Device activity log (?from=&to=&type=&limit=, dates in RFC3339) |
GET /client/{id}/subscriptions | Channels muted on the device |
PUT /client/{id}/subscriptions | Replace the list of muted channels, body {"channelIds": [1, 2]} (an empty array unmutes all) |
Device self-service (authenticated with the device’s own token):
| Method and path | Description |
|---|---|
GET /device/me/subscriptions | Channels muted on this device |
POST /device/me/subscriptions | Mute a channel, body {"channelId": 1} |
DELETE /device/me/subscriptions/{channel_id} | Unmute a channel |
Current user
Section titled “Current user”GET /current/user — current user information
Section titled “GET /current/user — current user information”curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/current/user{ "id": 1, "email": "user@example.com", "admin": false, "verified": true, "plan": "free", "eventCounter": 120, "messageCounter": 80, "heartbeatCounter": 30, "monitorCounter": 10, "webScriptCounter": 0, "balanceKopecks": 0, "storageBytes": 0, "image": "", "mfaEnabled": false}POST /current/user/sudo — confirm your identity (sudo)
Section titled “POST /current/user/sudo — confirm your identity (sudo)”Sensitive actions (changing the password, 2FA settings, admin endpoints) require a fresh
confirmation. Without it the response is 403 with "reason": "sudo_required" and the list of
available methods. A confirmation opens a 15-minute window for this device.
Device token only: no window is issued to an API token or a channel token (403).
| Field | Description |
|---|---|
method | password — if 2FA is off; totp or recovery — if it is on (the password is not accepted) |
pass | Password (for password) |
code | Authenticator code or recovery code |
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/current/user/sudo \ -H "Content-Type: application/json" \ -d '{"method": "password", "pass": "current-password"}'{"ok": true, "method": "password", "expiresIn": 900}Wrong password — 400 wrong password, wrong code — 401 invalid code.
POST /current/user/password — change password
Section titled “POST /current/user/password — change password”Requires a device token and a sudo window; an API token gets 403. The current password
oldPass is required, the new pass must be at least 8 characters and no longer than 72 bytes.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/current/user/password \ -H "Content-Type: application/json" \ -d '{"pass": "new-secure-password", "oldPass": "current-password"}'A wrong oldPass returns 400 with errorDescription: "wrong password". After a successful
change all other devices are signed out (their tokens are rotated), while the session the
request came from keeps working; open sudo windows are closed. Changing the password does not
touch API tokens — unlike a reset by email.
Two-factor authentication (profile)
Section titled “Two-factor authentication (profile)”All endpoints require a device token; those marked “sudo” require a sudo window.
| Method and path | Description |
|---|---|
GET /current/user/mfa | State: enabled, pendingSetup, recoveryCodesRemaining, mobileConnected, mobileEnabled, approverDevices |
POST /current/user/mfa/setup | sudo. A new authenticator secret: {"secret", "uri"} (otpauth://…); 2FA already enabled — 409 |
POST /current/user/mfa/confirm | sudo. Enable 2FA with the first code, body {"code": "123456"}; response — {"recoveryCodes": [...]} |
POST /current/user/mfa/disable | sudo. Disable 2FA |
POST /current/user/mfa/recovery-codes | sudo. Regenerate recovery codes |
POST /current/user/mfa/mobile/start | sudo. Start linking a phone for sign-in approval: {"challenge", "code", "expiresIn"} |
POST /current/user/mfa/mobile/confirm | Finish linking after the phone has entered code via /auth/mfa/approve, body {"challenge": "…"} |
POST /current/user/mfa/mobile/disable | sudo. Turn off sign-in approval from a phone |
DELETE /current/user/mfa/mobile/devices/{id} | sudo. Remove a device from the approvers |
Plan, balance and quotas
Section titled “Plan, balance and quotas”Details on plans and limits are on the Quotas and plans page.
| Method and path | Description |
|---|---|
GET /user/quota-breakdown | Detailed breakdown of event usage for a day (?day=YYYY-MM-DD), per hour and per minute, plus a snapshot of used storage (storage) |
POST /current/user/plan | Change plan, body {"plan": "free|pro|business"}. On your own you can only downgrade or keep the current plan; an upgrade — 402 plan upgrade requires payment |
POST /current/user/topup | Top up the balance, body {"amountRubles": N}. For a non-admin — 402 with payment details (paymentMethod: "bank_transfer") |
POST /current/user/image | Upload an avatar (multipart/form-data, field file) |
DELETE /current/user/image | Delete the avatar |
There is no payment gateway: plan upgrades and top-ups are paid by invoice (bank transfer), after
which an operator moves the account to the new plan (POST /user/{id}/plan).
curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/user/quota-breakdown?day=2026-06-23"
# Downgrade to freecurl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/current/user/plan \ -H "Content-Type: application/json" -d '{"plan": "free"}'POST /current/user/topup response for a non-admin (message is a human-readable instruction):
{ "message": "…", "amountRubles": 500, "paymentMethod": "bank_transfer"}Quiet hours
Section titled “Quiet hours”Details — Quiet hours and digest.
| Method and path | Description |
|---|---|
GET /current/user/quiet-hours | Current settings (for a new user: enabled: false, 23:00–08:00) |
PUT /current/user/quiet-hours | Save; fields not sent keep their values; the response is the saved object |
Body: enabled, timezone (IANA), start/end (HH:MM; an end earlier than the start
spans midnight), days (ISO 1=Mon…7=Sun, by the start day), breakthroughPriority
(0..10), digest.
curl -X PUT -H "X-Notifly-Key: $TOKEN" -H "Content-Type: application/json" \ https://api.notifly.ru/current/user/quiet-hours \ -d '{"enabled":true,"timezone":"Europe/Moscow","start":"23:00","end":"08:00","days":[1,2,3,4,5,6,7],"breakthroughPriority":8,"digest":true}'User management (administrator)
Section titled “User management (administrator)”GET /user — list of users
Section titled “GET /user — list of users”Paginated, keyset by ascending id: limit (1–200, default 100) and since — the id of the last
user on the previous page.
curl -H "X-Notifly-Key: $TOKEN" "https://api.notifly.ru/user?limit=100"{ "paging": {"size": 2, "limit": 100}, "users": [ { "id": 1, "email": "admin@example.com", "admin": true, "verified": true, "plan": "business", "mfaEnabled": true }, { "id": 2, "email": "user1@example.com", "admin": false, "verified": true, "plan": "free", "mfaEnabled": false } ]}A user has the same format as in GET /current/user (fields are abbreviated in the example).
POST /user — create a user
Section titled “POST /user — create a user”A user created by an administrator is verified immediately; on self-registration a verification email is sent to the address.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/user \ -H "Content-Type: application/json" \ -d '{"email": "newuser@example.com", "pass": "password123", "admin": false}'POST /user/{id} — edit a user
Section titled “POST /user/{id} — edit a user”Body: email (required, the current address — it cannot be changed), admin (send the current
value: a missing field means false), pass (optional, a new password), resetMfa (reset the
user’s 2FA). A password change by an administrator works like a reset: all of the user’s devices
are signed out and all of their API tokens are deleted.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/user/2 \ -H "Content-Type: application/json" \ -d '{"email": "user1@example.com", "admin": false, "pass": "new-password-123"}'POST /user/{id}/plan — change a user’s plan
Section titled “POST /user/{id}/plan — change a user’s plan”An operator plan change, including an upgrade after payment. Body {"plan": "free|pro|business"}.
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/user/2/plan \ -H "Content-Type: application/json" -d '{"plan": "pro"}'DELETE /user/{id} — delete a user
Section titled “DELETE /user/{id} — delete a user”You cannot delete yourself (400).
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/user/2 -X DELETEHeartbeat monitors
Section titled “Heartbeat monitors”Dead-man switch: an external job (cron, script) periodically sends a “ping”; if the ping doesn’t arrive in time, Notifly sends an alert. Details — Heartbeat.
Public ping (authentication by the H… ping token in the URL, no API token):
curl https://api.notifly.ru/heartbeat/ping/HrvF2iYjEnz5z8ZngHvvkX3 # GET — handy from croncurl -X POST https://api.notifly.ru/heartbeat/ping/HrvF2iYjEnz5z8ZngHvvkX3 \ -H "Content-Type: application/json" -d '{"fail_reason": "backup failed"}'Management (API token):
| Method and path | Description |
|---|---|
GET /heartbeat | List of heartbeats |
POST /heartbeat | Create |
PUT /heartbeat/{id} | Update |
DELETE /heartbeat/{id} | Delete |
POST /heartbeat/{id}/pause · /resume | Pause / resume |
POST /heartbeat/{id}/test-alert · /test-recovery | Test alert / recovery notification |
Monitors
Section titled “Monitors”Active availability and content checks. Each type has its own page with details.
Generic monitor (HTTP / TCP) → /docs/monitor/
Section titled “Generic monitor (HTTP / TCP) → /docs/monitor/”| Method and path | Description |
|---|---|
GET /monitor · POST /monitor | List / create |
POST /monitor/test | Test settings without saving |
PUT /monitor/{id} · DELETE /monitor/{id} | Update / delete |
POST /monitor/{id}/pause · /resume | Pause / resume |
| Method and path | Description |
|---|---|
GET /http-monitor · POST /http-monitor | List / create |
POST /http-monitor/test | Test a request |
PUT /http-monitor/{id} · DELETE /http-monitor/{id} | Update / delete |
POST /http-monitor/{id}/pause · /resume | Pause / resume |
| Method and path | Description |
|---|---|
GET /content-monitor · POST /content-monitor | List / create |
POST /content-monitor/suggest-selector | AI-suggested CSS selector |
POST /content-monitor/test-rule | Test a rule |
PUT /content-monitor/{id} · DELETE /content-monitor/{id} | Update / delete |
POST /content-monitor/{id}/pause · /resume | Pause / resume |
| Method and path | Description |
|---|---|
GET /port-monitor · POST /port-monitor | List / create |
POST /port-monitor/test | Test a port |
PUT /port-monitor/{id} · DELETE /port-monitor/{id} | Update / delete |
POST /port-monitor/{id}/pause · /resume | Pause / resume |
| Method and path | Description |
|---|---|
GET /port-scan · GET /port-scan/{id} | List / a single scan |
POST /port-scan | Start a scan |
PATCH /port-scan/{id} · DELETE /port-scan/{id} | Update / delete |
POST /port-scan/{id}/cancel · /restart | Cancel / restart |
POST /port-scan/{id}/to-monitor | Turn a discovered port into a monitor |
Workflow monitor → /docs/workflow-monitor/
Section titled “Workflow monitor → /docs/workflow-monitor/”| Method and path | Description |
|---|---|
GET /workflow-monitor · POST /workflow-monitor | List / create |
POST /workflow-monitor/build-step | AI step builder |
POST /workflow-monitor/test | Run the scenario |
PUT /workflow-monitor/{id} · DELETE /workflow-monitor/{id} | Update / delete |
POST /workflow-monitor/{id}/pause · /resume | Pause / resume |
Browser workflow → /docs/browser-workflow/
Section titled “Browser workflow → /docs/browser-workflow/”| Method and path | Description |
|---|---|
GET /browser-workflow · POST /browser-workflow | List / create |
POST /browser-workflow/build-step | AI-suggested action |
POST /browser-workflow/login | Capture auth cookies |
POST /browser-workflow/test-step | Test a step |
PUT /browser-workflow/{id} · DELETE /browser-workflow/{id} | Update / delete |
POST /browser-workflow/{id}/pause · /resume | Pause / resume |
Browser check screenshots
Section titled “Browser check screenshots”GET /browser-shot/{key} returns a JPEG screenshot of a failed browser workflow or browser
content-monitor check. The key comes from the notifly::screenshot field in the alert
message’s extras. Only the monitor’s owner has access; for <img> the token can be passed in
?token=. Old screenshots are removed by retention — then 404.
curl -H "X-Notifly-Key: $TOKEN" -o shot.jpg \ "https://api.notifly.ru/browser-shot/<value of notifly::screenshot>"Proof of host ownership before scanning/monitoring.
| Method and path | Description |
|---|---|
GET /verified-host | List |
POST /verified-host/start · /check | Start / check the verification |
DELETE /verified-host/{id} | Delete |
Monitor check history
Section titled “Monitor check history”{kind} — monitor type (monitor, http-monitor, content-monitor, port-monitor, etc.).
| Method and path | Description |
|---|---|
GET /monitor-history/{kind}/{id} | Uptime %, response time, aggregates |
GET /monitor-history/{kind}/{id}/log | Detailed log of individual checks |
Email inbox (Email Inbox)
Section titled “Email inbox (Email Inbox)”An incoming email to the inbox address becomes a notification. Details — Email Inbox.
| Method and path | Description |
|---|---|
GET /email-inbox · POST /email-inbox | List / create an inbox |
PUT /email-inbox/{id} · DELETE /email-inbox/{id} | Update / delete |
GET /email-inbox/{id}/rules · POST /email-inbox/{id}/rules | Email processing rules |
PUT /email-inbox/{id}/rules/{rid} · DELETE /email-inbox/{id}/rules/{rid} | Update / delete a rule |
GET /email-inbox/event | History of incoming emails |
GET /email-inbox/event/{eid} · DELETE /email-inbox/event/{eid} | An email / delete |
POST /email-inbox/rule/from-event/{eid} | AI-suggested rule from an email |
Web scripts
Section titled “Web scripts”JS snippets for your site: error tracking (console_errors), events, etc. Details — Web script.
Public ingest (authentication by the token in the URL, sent from the browser). GET and a
bodiless POST work too — the script fires with its default settings:
curl -X POST https://api.notifly.ru/script/<token> \ -H "Content-Type: application/json" -d '{ ... }'curl https://api.notifly.ru/script/<token>Management (API token):
| Method and path | Description |
|---|---|
GET /web-script · POST /web-script | List / create |
PUT /web-script/{id} · DELETE /web-script/{id} | Update / delete |
GET /web-script/{id}/sourcemaps · POST /web-script/{id}/sourcemaps | Source maps |
DELETE /web-script/{id}/sourcemaps/{smid} | Delete a source map |
GET /web-script/{id}/issues | Grouped errors (issues) |
GET /web-script/{id}/issue-count | Number of unresolved issues ({"unresolved": N}) |
POST /web-script/{id}/issues/{iid}/resolve · /ignore | Mark an issue resolved / ignored |
DELETE /web-script/{id}/issues/{iid} | Delete an issue |
GET /web-script/{id}/issues/{iid}/breadcrumbs | User actions before the error |
POST /web-script/{id}/issues/{iid}/analyze | AI root-cause analysis (body: lang, force) |
GET /web-script/{id}/issues/{iid}/analysis | Saved analysis (204 if there is none) |
GET /web-script/{id}/series · /error-series | Time series of triggers and errors (?bucket=1m|1h|1d&from=&to=) |
GET /web-script/{id}/flush-stats | Distribution of batch flush reasons |
Webhook router
Section titled “Webhook router”Receives incoming webhooks and routes them to channels by rules. It is a per-user singleton with path-based routing. Details — Webhook router.
Public ingest (authentication by the R… token in the URL, any path after the token; a
bodiless GET is accepted too):
curl -X POST https://api.notifly.ru/router/<token>/any/path \ -H "Content-Type: application/json" -d '{ ... }'Management (API token):
| Method and path | Description |
|---|---|
GET /webhook-router · PUT /webhook-router | Get / update the router |
POST /webhook-router/rotate-token | Rotate the public token |
GET /webhook-router/rule · POST /webhook-router/rule | Routing rules |
PUT /webhook-router/rule/{rid} · DELETE /webhook-router/rule/{rid} | Update / delete a rule |
GET /webhook-router/event · GET /webhook-router/event/{eid} | Event history |
DELETE /webhook-router/event/{eid} | Delete an event |
GET /webhook-router/message · DELETE /webhook-router/message/{mid} | Routed messages |
POST /webhook-router/rule/from-event/{eid} | AI-suggested rule from an event |
Metrics
Section titled “Metrics”Numeric time series with alerts. Details — Metrics.
Public metric ingest (authentication by the source’s M… token in the URL — not an API token):
curl -X POST https://api.notifly.ru/metric/<token> \ -H "Content-Type: application/json" -d '{ ... }'Management (API token):
| Method and path | Description |
|---|---|
GET /metric-source · POST /metric-source | Metric sources |
PUT /metric-source/{id} · DELETE /metric-source/{id} | Update / delete a source |
GET /metric-source/{id}/metrics | Metrics of a source |
PATCH /metric-source/{id}/metrics/{mid} · DELETE /metric-source/{id}/metrics/{mid} | Type / delete a metric |
GET /metric-source/{id}/metrics/{mid}/series | Time series of values |
GET /metric-source/{id}/alerts · POST /metric-source/{id}/alerts | Metric alerts |
PUT /metric-source/{id}/alerts/{aid} · DELETE /metric-source/{id}/alerts/{aid} | Update / delete an alert |
Delegated access (Channel Shares)
Section titled “Delegated access (Channel Shares)”Granting other users access to a channel by email. Details — Channel sharing.
| Method and path | Description |
|---|---|
GET /application/{id}/share · POST /application/{id}/share | List / create a share for a channel |
PUT /share/{id} · DELETE /share/{id} | Update / revoke a share |
GET /share/incoming | Shares granted to me |
GET /share/recipients | Addresses I have already shared with (suggestions) |
POST /share/{id}/accept · /decline | Accept / decline a share |
POST /share/join | Join by token |
POST /share/leave | Give up access to a channel, body {"appid": N} |
POST /share/rotate | Rotate the value of your Share token, body {"appid": N} |
Machine keys of a channel
Section titled “Machine keys of a channel”Multiple keys per channel with separate permissions, soft revocation and rotation. Available only to the channel owner.
| Method and path | Description |
|---|---|
GET /application/{id}/token · POST /application/{id}/token | List / issue a key. Body: name, canRead, canWrite, note, expiresAt (unix ms, 0 — no expiry) |
PUT /token/{id} | Change permissions, label and expiry |
DELETE /token/{id} | Revoke a key (a repeated call deletes the record; the channel’s primary key → 409) |
POST /token/{id}/rotate | Change the value, keeping the id and permissions |
Public status pages
Section titled “Public status pages”A public page with the status and uptime of a channel’s monitors. Details — Status pages.
Management (API token, channel owner only):
| Method and path | Description |
|---|---|
GET /status-page | List of your status pages |
POST /status-page | Create. Body: appid, slug, title, description, enabled, lang (ru|en), brandingHidden |
PUT /status-page/{id} | Update (same payload) |
DELETE /status-page/{id} | Delete |
Public endpoints (no authentication):
| Method and path | Description |
|---|---|
GET /status/{slug} | HTML status page |
GET /status/{slug}/summary | The same snapshot as JSON: overall, overallUptime90d, services[], incidents[] |
POST /status/{slug}/subscribe | Email subscription (double opt-in), body {"email": "..."} |
GET /status-subscribe/verify?token=… | Confirm the subscription (link from the email) |
GET /status-subscribe/unsub?token=… | Unsubscribe (link from the email) |
curl -H "X-Notifly-Key: $TOKEN" https://api.notifly.ru/status-page \ -H "Content-Type: application/json" \ -d '{"appid": 1, "slug": "acme-status", "title": "ACME Status", "enabled": true, "lang": "en"}'
curl https://api.notifly.ru/status/acme-status/summaryCodes: 400 — invalid slug/language or someone else’s channel, 403 — plan limit or a premium
field not included in the plan, 404 — page not found or not published, 409 — slug taken.
Maintenance windows
Section titled “Maintenance windows”Planned work: covered monitors don’t alert, messages aren’t escalated and arrive silently, the status page shows “Planned maintenance”. Details — Maintenance windows.
| Method and path | Description |
|---|---|
GET /maintenance-window | List of your windows |
POST /maintenance-window | Create |
GET /maintenance-window/{id} | A single window |
PUT /maintenance-window/{id} | Replace the fields |
DELETE /maintenance-window/{id} | Delete (204) |
Body: name, channelIds[], includeSubtree, monitors[] ({"kind","id"}), schedule
(once — startsAt/endsAt; weekly — timezone, days, start, end), enabled. The
response additionally contains active, nextStart, nextEnd, coveredChannelIds.
curl -X POST -H "X-Notifly-Key: $TOKEN" -H "Content-Type: application/json" \ https://api.notifly.ru/maintenance-window \ -d '{"name":"Release","channelIds":[12],"includeSubtree":true,"monitors":[],"schedule":"once","startsAt":"2026-10-02T23:00:00Z","endsAt":"2026-10-03T01:00:00Z","enabled":true}'Codes: 400 — validation error or someone else’s channel/monitor, 403 — limit or an unsuitable
API token, 404 — the window doesn’t exist or belongs to someone else.
API tokens
Section titled “API tokens”Programmatic account keys (M…) for the SDK, CLI and MCP.
Issued from the admin UI; details are on the API tokens page.
They can be managed only with a device token: an API token itself is not allowed on these
endpoints (403).
| Method and path | Description |
|---|---|
GET /api-token | List; the token field is a M…ab3d hint, not a secret |
POST /api-token | Issue; the only response with the full value |
PUT /api-token/{id} | Change name, access, scope, expiry |
POST /api-token/{id}/rotate | Reissue: a new value, the same settings |
DELETE /api-token/{id} | Delete |
POST/PUT body:
| Field | Type | Description |
|---|---|---|
name | string | Name (required) |
access | string | read or write (required) |
appIds | int[] | Allowed channels by id |
appTags | string[] | Allowed channels by tag; empty appIds and appTags — all channels. In PUT a missing field means no change |
expiresAt | string | Expiry, RFC3339, in the future; in PUT a missing field means no change |
curl -H "X-Notifly-Key: $DEVICE_TOKEN" https://api.notifly.ru/api-token \ -H "Content-Type: application/json" \ -d '{"name": "ci", "access": "write", "appTags": ["prod"], "expiresAt": "2027-01-01T00:00:00Z"}'Channel questions (Ask)
Section titled “Channel questions (Ask)”Interactive questions: the channel asks — the user answers from the interface. Details — Ask.
Asking a question uses the app token:
| Method and path | Authentication | Description |
|---|---|---|
POST /ask | app token | Ask a question on behalf of the channel |
GET /ask/pending-answers | app token | Fetch ready answers |
POST /ask/ack | app token | Acknowledge receipt of answers |
GET /ask-question/{id} | app token | Get the answer to a specific question (expired or cancelled — 410) |
POST /ask-question/{id}/answer | API token | Answer a question from the UI |
DELETE /ask-question/{id} | API token | Cancel a question |
POST /message/{id}/answer | API token | Answer a question from a message |
POST /ask-question/{id}/extend | device, API or channel token | Extend the answer deadline, body {"timeout_sec": 1…86400}; a channel token needs write permission |
GET /a/answer?token=… | signed link | Answer via a button from an email or Telegram |
GET /m/read?token=… | signed link | Mark a message as read (stops re-delivery) |
AI assistant
Section titled “AI assistant”A chat copilot for onboarding and help. Details — Assistant.
| Method and path | Description |
|---|---|
GET /assistant/threads | List of conversations |
GET /assistant/threads/{id}/messages | Message history of a conversation |
DELETE /assistant/threads/{id} | Delete a conversation |
POST /assistant/chat | Send a message to the assistant |
GET /assistant/insights | Proactive account recommendations (?polish=1, ?refresh=1, ?lang=) |
Real-time message stream
Section titled “Real-time message stream”There are two independent live-update channels. Details are on the WebSocket page.
WebSocket (/ws)
Section titled “WebSocket (/ws)”The primary channel for real-time push delivery. Connect with a token in the token query
parameter (usually a device token; other options are on the WebSocket page):
# Using wscatwscat -c "wss://api.notifly.ru/ws?token=CK2UDmhduKshCyz-RtqxiLx"const ws = new WebSocket('wss://api.notifly.ru/ws?token=CK2UDmhduKshCyz-RtqxiLx');ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log(`[${msg.title}] ${msg.message} (priority: ${msg.priority})`);};Every incoming event is a Message JSON object:
{ "id": 124, "appid": 1, "message": "New message", "title": "Title", "priority": 5, "date": "2025-06-01T10:31:00Z"}SSE (/stream)
Section titled “SSE (/stream)”A separate Server-Sent Events endpoint (GET /stream, Content-Type: text/event-stream)
for live UI updates: it signals that something has changed, while the messages themselves arrive
over WebSocket. The server sends only named events — connected,
channel:status_changed, share:changed (data — {"appid": N}) — plus a keepalive comment every
25 seconds. An onmessage handler does not receive them: subscribe with addEventListener.
Authentication — a token in ?token= or the session cookie on a page of the same site. For
cross-origin requests the cookie is not sent (withCredentials works only for origins listed in
the server’s CORS_ALLOWED_ORIGINS setting), so from elsewhere use ?token=.
const es = new EventSource('https://api.notifly.ru/stream?token=CK2UDmhduKshCyz-RtqxiLx');es.addEventListener('connected', () => console.log('connected'));es.addEventListener('channel:status_changed', (e) => { console.log('channel status changed', JSON.parse(e.data).appid);});es.addEventListener('share:changed', (e) => { console.log('channel access changed', JSON.parse(e.data).appid);});On AWS deployments /stream is unavailable and responds 503 — use WebSocket.
Examples in different languages
Section titled “Examples in different languages”Messages are sent only as a JSON body.
Python
Section titled “Python”import requests
API = "https://api.notifly.ru"
# Send a message (channel token)requests.post(f"{API}/message", params={"token": "ApNK7NiI0ltrJf3a8BNsOiD"}, json={ "title": "Backup", "message": "Backup completed", "priority": 2,})
# Fetch messages (API token)resp = requests.get(f"{API}/message", headers={"X-Notifly-Key": "MtVOKLDNuD7HR9y7QkMecC5"})for msg in resp.json()["messages"]: print(f"[{msg['title']}] {msg['message']}")package main
import ( "bytes" "encoding/json" "net/http")
func main() { body, _ := json.Marshal(map[string]any{ "title": "Deploy", "message": "Version 2.0 deployed", }) http.Post("https://api.notifly.ru/message?token=ApNK7NiI0ltrJf3a8BNsOiD", "application/json", bytes.NewReader(body))}JavaScript (Node.js)
Section titled “JavaScript (Node.js)”// Send a messageconst resp = await fetch('https://api.notifly.ru/message?token=ApNK7NiI0ltrJf3a8BNsOiD', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ title: 'CI', message: 'Tests passed', priority: 3, }),});console.log(await resp.json());PowerShell
Section titled “PowerShell”# Send a message$body = @{ title = "Report" message = "Daily report generated" priority = 1} | ConvertTo-JsonInvoke-RestMethod -Uri "https://api.notifly.ru/message?token=ApNK7NiI0ltrJf3a8BNsOiD" ` -Method POST -ContentType "application/json; charset=utf-8" -Body $bodyStatus codes
Section titled “Status codes”| Code | Meaning |
|---|---|
200 | Success |
204 | Success, no body (e.g. deleting a maintenance window) |
400 | Bad request (invalid parameters, wrong password on re-confirmation) |
401 | Unauthorized (missing or invalid token, second factor required, wrong 2FA code) |
402 | Payment required: plan upgrade, balance top-up, plan device limit |
403 | Forbidden: insufficient rights, unsuitable token type, sudo required ("reason": "sudo_required"), plan limit |
404 | Resource not found (including someone else’s) |
409 | Conflict: slug taken, question no longer pending, 2FA already enabled, revoking a channel’s primary key |
410 | No longer valid: question expired or cancelled, MFA challenge expired or used |
429 | Too many requests: daily event quota exhausted (daily event quota exceeded) or a rate limit hit (Retry-After header) |
503 | Temporarily unavailable: /health when the database is down, /stream on AWS |