Skip to content

Delivery & escalation

A push notification arrives instantly — but only if the phone is at hand. Delivery (the Delivery tab in the channel settings) solves the other half of the problem: what to do when nobody read the message.

Each channel can enable additional delivery methods. They work as an escalation: a message goes out over such a method only if the owner has not read it within a given time.

POST /message ──▶ push to every device (instantly)
└─ every minute: the delivery worker
├─ message already read? → do nothing
└─ method timeout elapsed? → email / SMS / call /
Slack / Telegram / webhook
MethodWhat you configureWhat arrives
Emailrecipient address, timeoutan email with a link to the message and action buttons
SMSphone number as +79001234567, timeouta short text with the channel name and the title
Callphone number, timeouta voice call with the notification
Slackincoming webhook URL, timeout, min. prioritya message in the Slack channel
Telegrambot token, chat ID, timeout, min. prioritya message from the bot (with inline buttons)
WebhookURL, optional secret, timeout, min. prioritya POST with a JSON body

Every method has its own delivery timeout (readTimeoutSec):

  • 0 — deliver on the next worker run (that is, within about a minute);
  • 300 — if the message has not been read for 5 minutes;
  • the maximum is 23 hours (82800).

The countdown starts from the moment the message was created, not from when the push was delivered. A message counts as read when you open it in the admin UI or the mobile app, when one of the endpoints POST /message/read / POST /application/{id}/message/read is called, or when you press ✓ Read in the email or in Telegram.

When a method is enabled, the server records a timestamp. Messages created before that moment are not escalated over the new method — otherwise enabling email in the evening would produce a mail about everything that piled up during the day.

Slack, Telegram and webhook have a min. priority field: messages with a priority below the given value are not sent over that adapter. Handy for keeping only important events in the work chat while the noise stays in the app.

Email, SMS and calls have no priority filter — they are controlled only by the toggle and the timeout.

Within a single run, messages are grouped by “owner + destination”:

  • email — a single message arrives as a regular email, several arrive as one digest email with a list;
  • SMS — one text per number: a title plus a short list of channels and subjects;
  • call — one call per number, regardless of how many messages there are.

Slack, Telegram and webhook are not grouped: one message — one send.

MethodBehaviour on failure
email, Slack, Telegram, webhookIdempotent: the delivery mark is written after sending, on failure the next run retries
SMS, callThe mark is written before sending (a duplicate is unacceptable); on error it is rolled back and the attempt is repeated

Each message goes out over each method exactly once: the delivery fact is recorded in a log and never fires twice.

Signed links are added to the email and to the Telegram bot message:

  • ✓ Read — marks the message as read and stops escalation over the remaining methods;
  • answer options — if the message contains a closed question (Ask), the buttons match the answer options.

The links are HMAC-signed and valid for a limited time (for questions — until the answer deadline), so they need no authentication in the UI.

A generic webhook receives a POST with JSON:

{
"title": "CPU high",
"message": "load average 12.4",
"priority": 8,
"channel": "Prod",
"appId": 12345,
"ts": 1782561600000
}

If a secret is set, the body is signed with HMAC-SHA256 and the signature is passed in a header:

X-Notifly-Signature: sha256=<hex>

Verifying it on the receiving side (Python):

import hmac, hashlib
def valid(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

Channel settings → the Delivery tab. Turn on the method you need with the toggle, fill in the address and the timeout. The Test button next to a method saves the settings and immediately sends a test notification there.

Telegram hint: create a bot via @BotFather (/newbot) — it returns a bot token; find the Chat ID (for direct messages ask @userinfobot, for a group it looks like -100…); make sure to press Start on your bot or add it to the group, otherwise Telegram will refuse the delivery.

Delivery settings are part of the channel object — the deliveryConfig field in POST /application and PUT /application/{id}:

Terminal window
curl -X PUT "$NOTIFLY_URL/application/12345" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <client-token>" \
-d '{
"name": "Prod",
"deliveryConfig": {
"email": { "enabled": true, "to": "oncall@example.com", "readTimeoutSec": 300 },
"telegram": { "enabled": true, "botToken": "123:ABC", "chatId": "-1001234567890",
"readTimeoutSec": 900, "minPriority": 7 },
"webhook": { "enabled": true, "url": "https://example.com/hook",
"secret": "s3cret", "readTimeoutSec": 0, "minPriority": 5 }
}
}'

Adapter fields:

FieldUsed byMeaning
enabledallwhether the method is on
toemail, sms, callrecipient address or number (+79001234567)
urlslack, webhookincoming webhook URL / arbitrary URL
botToken, chatIdtelegrambot parameters
secretwebhookHMAC-SHA256 signing key (optional)
readTimeoutSecallescalation timeout, 082800
minPriorityslack, telegram, webhookdo not send messages below this priority
Terminal window
curl -X POST "$NOTIFLY_URL/application/12345/delivery/test" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <client-token>" \
-d '{"adapter": "telegram"}'

Without a body, all enabled methods of the channel are checked. The response carries a result per method:

{ "results": { "telegram": { "ok": false, "error": "telegram: 403 bot was blocked by the user" } } }

If no method is enabled (or the requested adapter is off), the response is 400 no enabled adapter to test. The check bypasses escalation: no need to wait for a timeout, the notification goes out immediately.

Escalation does not consume the daily quota: the event was already charged when the message was created (message). No matter how many delivery methods are enabled, the event counter does not grow — see Quotas & pricing.

  • Slack and webhook URLs are checked by the SSRF guard: private, loopback and link-local addresses are blocked at send time.
  • Secrets (botToken, secret) live in the channel settings and are available to the channel owner only: someone with delegated access does not get the “Delivery” tab.
  • Managing delivery requires the channel owner’s client token; the channel app token is not enough.