Notification channels
Post PostStack events — new inbound mail, bounces, complaints, broken DNS — to a Slack channel, a Discord channel or a Telegram chat, formatted for people to read.
How channels work#
A notification channel is a sibling of a webhook. Every event PostStack dispatches to your webhooks is also offered to your channels; instead of a signed JSON body sent to your server, a channel gets a short message that PostStack formats for the platform — Block Kit for Slack, an embed for Discord, MarkdownV2 for Telegram.
- Each channel subscribes to a list of
events, and only those post. - Each channel has a scope — team-wide, one domain, or one mailbox — that decides which events it can see (see Scope).
- Deliveries are queued, tried up to 8 times, and recorded in the channel's delivery history; a channel that keeps failing is switched off automatically.
- A team can have up to 50 Slack, Discord and Telegram channels.
In the dashboard, channels live under Configuration → Notifications. Add channel walks you through platform, credentials, events and scope; each channel's menu has Send test, Edit, the delivery history and an enable/disable switch.
Connect a platform#
Slack
Create an incoming webhook
In Slack, add the Incoming Webhooks app to your workspace, pick the channel to post in, and copy the webhook URL.
Create the channel in PostStack
Use
type: "slack"and put the URL inconfig.webhookUrl. The host must be exactlyhooks.slack.com.
Discord
Create a webhook on the Discord channel
Open the channel's Edit Channel → Integrations → Webhooks, create a webhook and copy its URL.
Create the channel in PostStack
Use
type: "discord"andconfig.webhookUrl. The URL must behttps://discord.com/api/webhooks/<id>/<token>;discordapp.com,canary.discord.comandptb.discord.comare accepted too.
Telegram
Create a bot
Message
@BotFather, run/newbot, and copy the token it gives you (it looks like123456789:ABCdef…).Find the chat id
Add the bot to the group or channel, send a message there, then open
https://api.telegram.org/bot<token>/getUpdatesand readchat.id. Group and channel ids are negative, e.g.-1001234567890; a public channel can also be given as@yourchannel.Create the channel in PostStack
Use
type: "telegram"withconfig.botTokenandconfig.chatId. PostStack calls Telegram'sgetMewith the token before saving, so a wrong or revoked token is refused at creation with Telegram's own error text.
Events#
These eight events have a dedicated message format and are the ones the dashboard offers:
| Event | Dashboard label | Fires when | Message |
|---|---|---|---|
inbound_email.received | New inbound email | Mail is received for one of your inbound-enabled domains. | 📬 New email in <recipient> — sender, subject and the start of the text body; when it went to a mailbox, a link to open it in the dashboard. |
email.bounced | Email bounced | An email you sent hard-bounced. | ↩️ Email bounced |
email.complained | Spam complaint | A recipient reported your email as spam (feedback loop). | 🚩 Spam complaint |
email.suppressed | Address suppressed | A send skipped recipients because they are on your suppression list. | 🚫 Recipient suppressed |
domain.failed | Domain verification failed | A verified domain stops verifying and drops back to pending — sending from it is paused. | ❌ Domain verification failed — the domain and which records (SPF, DKIM…) no longer match. |
domain.dns_drift | DNS record drifted | A record of a verified domain no longer has the expected value. | ⚠️ <RECORD> record drifted — expected value and what is published now. |
deliverability.degraded | Deliverability degraded | Mail from a domain is signed with a key that cannot align for DMARC. | 📉 Deliverability dropped — the domain and the reason. |
webhook.disabled | Webhook auto-disabled | One of your webhook endpoints was disabled after repeated failures. | ⚠️ Webhook auto-disabled — the URL and the failure count. |
events also accepts any other webhook event — email.delivered, email.opened, domain.verified and so on. Those have no dedicated format: the message is titled 🔔 <event> and its body is the event payload as JSON, cut at 1,000 characters. Any other name is refused with 400 and an error listing the valid events.
Scope#
Set at most one of domainId and mailboxId (both numeric ids, as returned in the id field of the domain or mailbox). Setting both is a 400.
| Scope | Set | Receives |
|---|---|---|
| Team-wide | neither | Every subscribed event in the team. |
| Per-domain | domainId | Subscribed events that carry that domain: inbound mail to it, bounces, complaints and suppressions of mail sent from it, and its domain.* and deliverability.degraded events. |
| Per-mailbox | mailboxId | Only inbound mail delivered to that mailbox. |
Every matching channel fires — a team-wide channel and a per-mailbox channel both subscribed to inbound_email.received both post the same message. Events without a domain, such as webhook.disabled, only reach team-wide channels.
Delivery, retries and auto-disable#
- Each platform request times out after 10 seconds. Any non-2xx answer or network error counts as a failed attempt.
- A delivery is tried up to 8 times. The waits between attempts are 30 s, 2 min, 10 min, 30 min, 2 h, 8 h and 16 h — about 27 hours in all. When the platform answers
429with a retry hint (SlackRetry-After, DiscordX-RateLimit-Reset-After, Telegramretry_after), that wait is used instead. - When a delivery has used all 8 attempts, the channel's
consecutiveFailuresgoes up by one; any successful delivery resets it to 0. - At 20 consecutive failed deliveries the channel is disabled (
active: false, withdisabledAtanddisabledReasonset). The team owner is emailed and an in-app notification appears. Queued deliveries for a disabled channel are dropped with the noteChannel was disabled before delivery. - Re-enable it with Re-enable in the dashboard or
PUT /notification-channels/:publicId { "enabled": true }. That resetsconsecutiveFailuresand clears the disabled state. Missed events are not re-sent; replay individual deliveries from the history if you need them.
Authentication#
All endpoints take a full-access API key as Authorization: Bearer sk_live_…. A sending-only key gets 403 insufficient_scope. OAuth tokens need notifications:read for the read endpoints and notifications:manage for the rest; notifications:manage includes notifications:read.
Channels can also be managed from the SDKs: in TypeScript, client.notificationChannels has create, list, get, update, delete, test, getDeliveries and replayDelivery, and Python and Go have the same resource. The CLI and MCP server have no notification-channel commands.
Endpoints#
Base URL https://api.poststack.dev. Errors are returned as { "error": "…" }.
Create a channel#
Validates the platform credentials, encrypts the secrets and creates an active channel. Rate limited to 20 requests per minute.
https://api.poststack.dev/notification-channelscurl -X POST https://api.poststack.dev/notification-channels \
-H "Authorization: Bearer $POSTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "slack",
"name": "Support inbox",
"events": ["inbound_email.received"],
"mailboxId": 12,
"config": { "webhookUrl": "https://hooks.slack.com/services/T000/B000/XXXX" }
}'{
"type": "telegram",
"name": "Ops alerts",
"events": ["email.bounced", "domain.failed"],
"config": {
"botToken": "123456789:ABCdef-GhIJklm_NopQRsTuvWXyz",
"chatId": "-1001234567890"
}
}{
"channel": {
"publicId": "nc_k3v9x2m7q8w4r1t6y5u0p2a9",
"type": "telegram",
"name": "Ops alerts",
"domainId": null,
"mailboxId": null,
"events": ["email.bounced", "domain.failed"],
"active": true,
"configSummary": { "botToken": "•••••", "chatId": "-1001234567890" },
"consecutiveFailures": 0,
"disabledAt": null,
"disabledReason": null,
"lastUsedAt": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:00:00.000Z"
}
}Body parameters
type"slack" | "discord" | "telegram"required- The platform. Cannot be changed later.
namestringrequired- 1–200 characters.
eventsstring[]required- At least one event type. See Events.
config.webhookUrlstring (URL)- Slack and Discord, required. Slack: host
hooks.slack.com. Discord:/api/webhooks/<id>/<token>ondiscord.com,discordapp.com,canary.discord.comorptb.discord.com. config.botTokenstring- Telegram, required. 20–200 characters, shaped
<digits>:<token>; checked with Telegram'sgetMe. config.chatIdstring- Telegram, required. 1–64 characters: a numeric chat id such as
-1001234567890, or@channelname. domainIdinteger- Scope the channel to this domain. Not together with mailboxId.
mailboxIdinteger- Scope the channel to this mailbox. Not together with domainId.
Response fields
channel.publicIdstring- Channel id,
nc_+ 24 characters. channel.type"slack" | "discord" | "telegram" | "web_push"- The platform.
web_pushchannels are the browser push subscriptions created from the dashboard; they are listed here but cannot be created through this API. channel.namestring- Your label for the channel.
channel.domainIdinteger | null- Set when the channel is scoped to one domain.
channel.mailboxIdinteger | null- Set when the channel is scoped to one mailbox.
channel.eventsstring[]- The event types the channel posts.
channel.activebooleanfalsewhen you disabled the channel or it was auto-disabled.channel.configSummaryobject- The config with secrets masked. Slack and Discord show the webhook URL with its last path segment replaced by
•••••; Telegram showsbotTokenas•••••and thechatIdin full. Stored secrets are encrypted and never returned. channel.consecutiveFailuresinteger- Deliveries that failed permanently in a row. Reset by any successful delivery.
channel.disabledAtstring (ISO 8601) | null- When the channel was auto-disabled.
channel.disabledReasonstring | null- For example
Auto-disabled after 20 consecutive failures. channel.lastUsedAtstring (ISO 8601) | null- Time of the last successful delivery or test.
channel.createdAtstring (ISO 8601)- Creation time.
channel.updatedAtstring (ISO 8601)- Last change.
Status codes
| Status | Meaning |
|---|---|
| 201 | Created. The body is { channel }. |
| 400 | Body failed validation; both domainId and mailboxId set; or the platform check failed — e.g. Slack webhook URL must use host hooks.slack.com, the Discord URL shape, or Telegram's getMe error such as Unauthorized. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope (sending-only key, or OAuth token without notifications:manage), or Notification channel limit reached (50 per team). |
| 404 | Domain not found / Mailbox not found — the id is not in your team. |
| 429 | More than 20 creates in a minute. |
List channels#
All of the team's channels, newest first, including web_push channels. Not paginated.
https://api.poststack.dev/notification-channelscurl https://api.poststack.dev/notification-channels \
-H "Authorization: Bearer $POSTSTACK_API_KEY"{
"channels": [
{
"publicId": "nc_k3v9x2m7q8w4r1t6y5u0p2a9",
"type": "telegram",
"name": "Ops alerts",
"domainId": null,
"mailboxId": null,
"events": ["email.bounced", "domain.failed"],
"active": true,
"configSummary": { "botToken": "•••••", "chatId": "-1001234567890" },
"consecutiveFailures": 0,
"disabledAt": null,
"disabledReason": null,
"lastUsedAt": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:00:00.000Z"
}
]
}Response fields
channels[]object[]- Channel objects, same fields as
channelin Create a channel.
Status codes
| Status | Meaning |
|---|---|
| 200 | The channels (possibly an empty array). |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
Get a channel#
One channel.
https://api.poststack.dev/notification-channels/:publicIdcurl https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9 \
-H "Authorization: Bearer $POSTSTACK_API_KEY"{
"channel": {
"publicId": "nc_k3v9x2m7q8w4r1t6y5u0p2a9",
"type": "telegram",
"name": "Ops alerts",
"domainId": null,
"mailboxId": null,
"events": ["email.bounced", "domain.failed"],
"active": true,
"configSummary": { "botToken": "•••••", "chatId": "-1001234567890" },
"consecutiveFailures": 0,
"disabledAt": null,
"disabledReason": null,
"lastUsedAt": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:00:00.000Z"
}
}Path parameters
publicIdstringrequired- The channel id,
nc_followed by 24 lowercase letters or digits. Any other shape is rejected with400 Invalid notification channel ID.
Response fields
channelobject- The channel.
Status codes
| Status | Meaning |
|---|---|
| 200 | The channel, as { channel }. |
| 400 | Invalid notification channel ID. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
| 404 | Notification channel not found. |
Update a channel#
Partial update despite the PUT: only the fields you send change. The type cannot be changed. Rate limited to 60 requests per minute.
https://api.poststack.dev/notification-channels/:publicIdcurl -X PUT https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9 \
-H "Authorization: Bearer $POSTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'{
"events": ["email.bounced", "email.complained"],
"enabled": true
}{
"channel": {
"publicId": "nc_k3v9x2m7q8w4r1t6y5u0p2a9",
"type": "telegram",
"name": "Ops alerts",
"domainId": null,
"mailboxId": null,
"events": ["email.bounced", "email.complained"],
"active": true,
"configSummary": { "botToken": "•••••", "chatId": "-1001234567890" },
"consecutiveFailures": 0,
"disabledAt": null,
"disabledReason": null,
"lastUsedAt": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:00:00.000Z"
}
}Path parameters
publicIdstringrequired- The channel id,
nc_followed by 24 lowercase letters or digits. Any other shape is rejected with400 Invalid notification channel ID.
Body parameters
namestring- 1–200 characters.
eventsstring[]- Replaces the whole list; at least one.
domainIdinteger | null- Set a domain scope, or
nullto clear it. mailboxIdinteger | null- Set a mailbox scope, or
nullto clear it. enabledbooleanfalsedisables the channel.trueenables it and, after an auto-disable, resetsconsecutiveFailures,disabledAtanddisabledReason.configobject (string values)- Merged into the stored config: keys you send overwrite, keys you leave out are kept — so
{ "config": { "botToken": "…" } }rotates the token and keeps the chat id. The merged result is validated again (Telegram:getMe). Refused forweb_pushchannels.
Response fields
channelobject- The updated channel.
Status codes
| Status | Meaning |
|---|---|
| 200 | Updated (or unchanged when the body was empty), as { channel }. |
| 400 | Validation failed, the result would have both a domain and a mailbox scope, the new config failed the platform check, or config was sent for a web_push channel. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
| 404 | Notification channel not found, or Domain / Mailbox not found. |
| 429 | More than 60 updates or deletes in a minute. |
Delete a channel#
Deletes the channel and its whole delivery history. Cannot be undone. Rate limited to 60 requests per minute.
https://api.poststack.dev/notification-channels/:publicIdcurl -X DELETE https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9 \
-H "Authorization: Bearer $POSTSTACK_API_KEY"{ "success": true }Path parameters
publicIdstringrequired- The channel id,
nc_followed by 24 lowercase letters or digits. Any other shape is rejected with400 Invalid notification channel ID.
Response fields
successboolean- Always
true.
Status codes
| Status | Meaning |
|---|---|
| 200 | Deleted. |
| 400 | Invalid notification channel ID. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
| 404 | Notification channel not found. |
| 429 | More than 60 updates or deletes in a minute. |
Send a test message#
Posts “✅ PostStack notification test” to the platform synchronously and records it in the delivery history as event type test. Rate limited to 10 requests per minute.
https://api.poststack.dev/notification-channels/:publicId/testcurl -X POST https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9/test \
-H "Authorization: Bearer $POSTSTACK_API_KEY"{
"success": true,
"statusCode": 200,
"responseBody": "ok"
}Path parameters
publicIdstringrequired- The channel id,
nc_followed by 24 lowercase letters or digits. Any other shape is rejected with400 Invalid notification channel ID.
Response fields
successbooleantrueonly when the platform accepted the message;falsewhen it rejected it or could not be reached — seestatusCodeandresponseBody.statusCodeinteger | null- The platform's HTTP status;
nullon a network error or timeout. responseBodystring | null- The platform’s response (or the network error), first 500 characters.
Status codes
| Status | Meaning |
|---|---|
| 200 | The attempt was made; check statusCode. |
| 400 | Invalid notification channel ID, or Channel is disabled. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
| 404 | Notification channel not found. |
| 429 | More than 10 tests in a minute. |
List deliveries#
The channel's delivery history, newest first, including test messages.
https://api.poststack.dev/notification-channels/:publicId/deliveriescurl "https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9/deliveries?page=1&per_page=20" \
-H "Authorization: Bearer $POSTSTACK_API_KEY"{
"deliveries": [
{
"id": 5812,
"channelId": 31,
"eventType": "email.bounced",
"payload": {
"type": "email.bounced",
"created_at": "2026-09-29T10:14:02.114Z",
"message": { "eventType": "email.bounced", "title": "↩️ Email bounced", "subtitle": "…", "body": "…" },
"raw": { "email_id": "em_…", "bounce_code": "550", "…": "…" },
"scope": { "domainId": 42 }
},
"statusCode": 200,
"responseBody": "{\"ok\":true,…}",
"attempts": 1,
"nextRetryAt": null,
"deliveredAt": "2026-09-29T10:14:02.530Z",
"createdAt": "2026-09-29T10:14:02.114Z"
}
],
"meta": { "page": 1, "perPage": 20, "total": 1, "totalPages": 1 }
}Path parameters
publicIdstringrequired- The channel id,
nc_followed by 24 lowercase letters or digits. Any other shape is rejected with400 Invalid notification channel ID.
Query parameters
pageintegerdefault1- Page number, from 1.
per_pageintegerdefault20- 1–100.
Response fields
deliveries[].idinteger- Delivery id, used to replay it.
deliveries[].channelIdinteger- Internal channel id.
deliveries[].eventTypestring- The event, or
test. deliveries[].payloadobjecttype,created_at,message(the formattedtitle/subtitle/body/link), and for real eventsraw(the webhook payload) andscope.deliveries[].statusCodeinteger | null- Platform status of the last attempt.
deliveries[].responseBodystring | null- Platform response of the last attempt, up to 1,000 characters.
deliveries[].attemptsinteger- Attempts made so far (max 8).
deliveries[].nextRetryAtstring (ISO 8601) | null- When the next retry is due.
deliveries[].deliveredAtstring (ISO 8601) | null- Set on success;
nullwhile pending or after failing. deliveries[].createdAtstring (ISO 8601)- When the event was dispatched.
metaobjectpage,perPage,total,totalPages.
Status codes
| Status | Meaning |
|---|---|
| 200 | A page of deliveries. |
| 400 | Invalid channel ID or query parameters. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
| 404 | Notification channel not found. |
Replay a delivery#
Resets a delivery (attempts back to 0, status cleared) and queues it again with the message it was created with. Rate limited to 10 requests per minute.
https://api.poststack.dev/notification-channels/:publicId/deliveries/:deliveryId/replaycurl -X POST https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9/deliveries/5812/replay \
-H "Authorization: Bearer $POSTSTACK_API_KEY"{ "success": true }Path parameters
publicIdstringrequired- The channel id,
nc_followed by 24 lowercase letters or digits. Any other shape is rejected with400 Invalid notification channel ID. deliveryIdintegerrequired- The
idfrom List deliveries.
Response fields
successboolean- Always
true: the delivery was queued.
Status codes
| Status | Meaning |
|---|---|
| 200 | Queued. Watch the delivery history for the result. |
| 400 | Invalid channel ID or delivery id. |
| 401 | Missing or invalid API key. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs. |
| 404 | Notification channel not found, or Delivery not found on this channel. |
| 429 | More than 10 replays in a minute. |
Troubleshooting#
Send test returns success: false
The platform rejected the message or could not be reached. Look at statusCode and responseBody — they are the platform's own answer, so the reason is in there: a deleted Slack or Discord webhook, a Telegram chat id that does not exist, or a bot that is not a member of the chat.
A per-mailbox channel never fires for bounces
Per-mailbox channels only receive inbound mail for that mailbox. Put bounce and domain events on a team-wide or per-domain channel.
The channel switched itself off
It reached 20 permanently failed deliveries in a row. The reason is in disabledReason and the last platform response in the delivery history. Fix the webhook or bot, then re-enable the channel and use Send test.
An event I subscribed to never posts
Check the scope: events without a domain only reach team-wide channels.
Channels compared with webhooks#
Webhooks- A signed JSON payload to your own HTTPS endpoint, for code to act on. See Webhooks.
Notification channels- A formatted message to Slack, Discord or Telegram, for people to read. Same events, same retry schedule, no code to write.