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 / Discord / Teams / Mattermost / PagerDuty / OpsgenieDelivery methods
Section titled “Delivery methods”| Method | What you configure | What arrives |
|---|---|---|
| recipient address, timeout | an email with a link to the message and action buttons | |
| SMS | phone number as +79001234567, timeout | a short text with the channel name and the title |
| Call | phone number, timeout | a voice call with the notification |
| Slack | incoming webhook URL, timeout, min. priority | a message in the Slack channel |
| Telegram | bot token, chat ID, timeout, min. priority | a message from the bot (with inline buttons) |
| Webhook | URL, optional secret, timeout, min. priority | a POST with a JSON body |
| Discord | webhook URL, timeout, min. priority | an embed card with action links |
| Teams | workflow URL (Workflows), timeout, min. priority | an Adaptive Card with action buttons |
| Mattermost | incoming webhook URL, timeout, min. priority | a message with a coloured bar and links |
| PagerDuty | Integration Key, region, reaction to “read” | an incident (Events API v2) |
| Opsgenie | API key, region, reaction to “read” | an alert (Alerts API) |
Timeout: when exactly it fires
Section titled “Timeout: when exactly it fires”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, in Telegram or in another
integration. In the Android app the same is done by the
Mark read button right in the notification and by the acknowledge button on
the alarm-mode screen.
The “before it was enabled” cut-off
Section titled “The “before it was enabled” cut-off”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.
Minimum priority
Section titled “Minimum priority”Every integration (Slack, Telegram, webhook, Discord, Teams, Mattermost, PagerDuty, Opsgenie) has 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.
Grouping and digests
Section titled “Grouping and digests”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.
Integrations (Slack, Telegram, webhook, Discord, Teams, Mattermost, PagerDuty, Opsgenie) are not grouped: one message — one send.
Delivery guarantees
Section titled “Delivery guarantees”| Method | Behaviour on failure |
|---|---|
| email and integrations | Idempotent: the delivery mark is written after sending, on failure the next run retries |
| SMS, call | The 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.
Action buttons in email and Telegram
Section titled “Action buttons in email and Telegram”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 other integrations get the same actions: buttons in Slack and Teams, links
under the text in Discord and Mattermost, incident links in PagerDuty, lines of
the description in Opsgenie.
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.
Discord, Microsoft Teams and Mattermost
Section titled “Discord, Microsoft Teams and Mattermost”Chat integrations receive the message as a card coloured by priority (red —
9–10, orange — 7–8, yellow — 4–6, blue — 1–3) with a title, the text and a
Notifly · channel · P<priority> footer.
| Service | Where to send | What actions look like |
|---|---|---|
| Discord | channel webhook: https://discord.com/api/webhooks/<id>/<token> | markdown links under the text |
| Teams | URL of the “Post to a channel when a webhook request is received” workflow (Workflows / Power Automate) | Adaptive Card Action.OpenUrl buttons |
| Mattermost | incoming webhook: https://<server>/hooks/<key> | markdown links under the text |
- Discord. Discord channel settings → Integrations → Webhooks → New
Webhook → Copy Webhook URL. Only Discord hosts are accepted (
discord.com,discordapp.com).@everyone/@herein the message text do not ping (allowed_mentionsis empty). - Microsoft Teams. In the Teams channel: … → Workflows → the Post to a
channel when a webhook request is received template → pick the team and
channel → copy the URL. The message arrives as an Adaptive Card (version 1.4).
Legacy Office 365 connectors (
*.webhook.office.com) are being retired by Microsoft — such URLs are rejected on save with a hint to switch to Workflows. - Mattermost. Integrations → Incoming Webhooks → Add Incoming Webhook.
The format is Slack-compatible (an attachment). Mass mentions
@channel,@all,@herein the text are neutralised.
PagerDuty and Opsgenie
Section titled “PagerDuty and Opsgenie”Incident integrations open an incident/alert rather than just posting to a chat.
| Notifly priority | PagerDuty severity | Opsgenie priority |
|---|---|---|
| 9–10 | critical | P1 |
| 7–8 | error | P2 |
| 4–6 | warning | P3 |
| 1–3 | info | P4 |
| 0 | info | P5 |
- PagerDuty — Events API v2. In the PagerDuty service: Integrations → Add
integration → Events API V2, copy the Integration Key (32 characters).
For accounts in the EU service region choose region
eu(events.eu.pagerduty.com).dedup_key=notifly-<message id>: re-sending the same message does not open a second incident. Action buttons arrive as the incident’slinks, the text ascustom_details.message. - Opsgenie — Alerts API (
POST /v2/alerts, headerAuthorization: GenieKey …). In the Opsgenie team: Integrations → Add integration → API, copy the API key; regionus(api.opsgenie.com) oreu(api.eu.opsgenie.com).alias=notifly-<message id>— Opsgenie deduplicates open alerts by it. Action links go to the description and todetails.
Reacting to “read”
Section titled “Reacting to “read””The “When the message is read” field (onRead) closes the loop: once the
message is read in Notifly — in the app, the admin UI or via the ✓ Read
(Read) link right from the incident — the delivery worker sends acknowledge or
resolve to PagerDuty, acknowledge or close to Opsgenie (by the same
dedup_key/alias) within a minute. By default ("") the incident is left
alone.
The reaction runs at most once per message: a failed attempt (after the built-in retries) is not repeated forever. There is no reverse direction: acknowledging in PagerDuty does not mark the message as read in Notifly.
Webhook format
Section titled “Webhook format”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 — the SDKs already have it.
Python:
import notifly
raw = request.get_data()if not notifly.verify_signature(SECRET, raw, request.headers.get("X-Notifly-Signature")): abort(401)Go — the webhook subpackage ships a handler that verifies the signature,
decodes the body and answers with the right status codes:
import "github.com/truboter/notifly.ru/sdk/go/webhook"
http.Handle("/notifly/alerts", webhook.DeliveryHandler(secret, func(ctx context.Context, p *webhook.DeliveryPayload) error { log.Printf("unread in %s: %s", p.ChannelName, p.Title) return nil // returning an error asks Notifly to retry }, &webhook.Options{Dedup: webhook.NewDedup(time.Hour)}))Without an SDK it is seven lines in any language: hmac_sha256(secret, raw_body) in hex, prefixed with sha256=, compared in constant time.
Configuration
Section titled “Configuration”From the admin UI
Section titled “From the admin UI”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.
Via the REST API
Section titled “Via the REST API”Delivery settings are part of the channel object — the deliveryConfig field in
POST /application and PUT /application/{id}:
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 }, "pagerduty": { "enabled": true, "routingKey": "R0123456789abcdef0123456789abcde", "region": "eu", "onRead": "resolve", "readTimeoutSec": 600, "minPriority": 8 } } }'Adapter fields:
| Field | Used by | Meaning |
|---|---|---|
enabled | all | whether the method is on |
to | email, sms, call | recipient address or number (+79001234567) |
url | slack, webhook, discord, teams, mattermost | webhook URL / arbitrary URL |
botToken, chatId | telegram | bot parameters |
secret | webhook | HMAC-SHA256 signing key (optional) |
readTimeoutSec | all | escalation timeout, 0…82800 |
routingKey | pagerduty | Integration Key (Events API v2) |
apiKey | opsgenie | API integration key |
region | pagerduty, opsgenie | us (default) or eu |
onRead | pagerduty, opsgenie | "", acknowledge or resolve |
minPriority | all integrations | do not send messages below this priority |
Testing delivery
Section titled “Testing delivery”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.
Quotas
Section titled “Quotas”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.
Security
Section titled “Security”- Slack, webhook, Discord, Teams and Mattermost URLs are checked by the SSRF guard: private, loopback and link-local addresses are blocked at send time.
- Secrets — the Slack, Discord, Teams and Mattermost webhook URLs (they embed a
token),
botToken,secret,routingKey,apiKey— are replaced with the********mask in API responses; sending the mask back means “keep”. They 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.
Quiet hours and maintenance windows
Section titled “Quiet hours and maintenance windows”Two mechanisms cancel escalation — for good, without a morning catch-up:
- Quiet hours of the channel owner: while the window is active, methods for messages below the urgency threshold are not sent; they are collected in a single digest instead.
- Maintenance windows: messages in covered channels are not escalated at all, regardless of priority.
A suppressed method is logged as “suppressed” and never retried.
If forwarding did not fire
Section titled “If forwarding did not fire”Check in this order:
- Is the message still unread? Reading it — in the dashboard, in the app or with the Mark read button in the notification — cancels escalation.
- Has the method’s timeout passed (see Timeout). Messages created before the method was enabled are not forwarded.
- Is the priority not below the minimum (
minPriority) — for the integrations where it is set (see Minimum priority). - Are quiet hours or a maintenance window in effect — they suppress escalation for good.
- Does the method itself work — run a delivery test: it sends a notification right away and returns the adapter’s error if something is wrong. SMS and phone calls fire only if the integration is enabled on the server side.
If the push notification itself does not reach your device, see Notifications not arriving.
See also
Section titled “See also”- Sending messages — priorities, titles and extras.
- Questions in channels (Ask) — answering right from the email or Telegram.
- Notifications not arriving — a checklist for push to devices.
- WebSocket protocol — instant receiving instead of escalation.
- Public status pages — notifications for your users rather than for your team.