Skip to content

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

  1. 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.

  2. Create the channel in PostStack

    Use type: "slack" and put the URL in config.webhookUrl. The host must be exactly hooks.slack.com.

Discord

  1. Create a webhook on the Discord channel

    Open the channel's Edit Channel → Integrations → Webhooks, create a webhook and copy its URL.

  2. Create the channel in PostStack

    Use type: "discord" and config.webhookUrl. The URL must be https://discord.com/api/webhooks/<id>/<token>; discordapp.com, canary.discord.com and ptb.discord.com are accepted too.

Telegram

  1. Create a bot

    Message @BotFather, run /newbot, and copy the token it gives you (it looks like 123456789:ABCdef…).

  2. Find the chat id

    Add the bot to the group or channel, send a message there, then open https://api.telegram.org/bot<token>/getUpdates and read chat.id. Group and channel ids are negative, e.g. -1001234567890; a public channel can also be given as @yourchannel.

  3. Create the channel in PostStack

    Use type: "telegram" with config.botToken and config.chatId. PostStack calls Telegram's getMe with the token before saving, so a wrong or revoked token is refused at creation with Telegram's own error text.

The bot must be allowed to post

getMe only proves the token is valid. If the bot is not a member of the chat (or not an admin of a channel), creation still succeeds but every message fails — use Send test right after creating the channel.

Events#

These eight events have a dedicated message format and are the ones the dashboard offers:

EventDashboard labelFires whenMessage
inbound_email.receivedNew inbound emailMail 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.bouncedEmail bouncedAn email you sent hard-bounced.↩️ Email bounced
email.complainedSpam complaintA recipient reported your email as spam (feedback loop).🚩 Spam complaint
email.suppressedAddress suppressedA send skipped recipients because they are on your suppression list.🚫 Recipient suppressed
domain.failedDomain verification failedA 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_driftDNS record driftedA record of a verified domain no longer has the expected value.⚠️ <RECORD> record drifted — expected value and what is published now.
deliverability.degradedDeliverability degradedMail from a domain is signed with a key that cannot align for DMARC.📉 Deliverability dropped — the domain and the reason.
webhook.disabledWebhook auto-disabledOne 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.

ScopeSetReceives
Team-wideneitherEvery subscribed event in the team.
Per-domaindomainIdSubscribed 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-mailboxmailboxIdOnly 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 429 with a retry hint (Slack Retry-After, Discord X-RateLimit-Reset-After, Telegram retry_after), that wait is used instead.
  • When a delivery has used all 8 attempts, the channel's consecutiveFailures goes up by one; any successful delivery resets it to 0.
  • At 20 consecutive failed deliveries the channel is disabled (active: false, with disabledAt and disabledReason set). The team owner is emailed and an in-app notification appears. Queued deliveries for a disabled channel are dropped with the note Channel was disabled before delivery.
  • Re-enable it with Re-enable in the dashboard or PUT /notification-channels/:publicId { "enabled": true }. That resets consecutiveFailures and 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.

POSThttps://api.poststack.dev/notification-channels
Request
curl -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" }
  }'
{
  "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> on discord.com, discordapp.com, canary.discord.com or ptb.discord.com.
config.botTokenstring
Telegram, required. 20–200 characters, shaped <digits>:<token>; checked with Telegram's getMe.
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_push channels 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.activeboolean
false when 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 shows botToken as ••••• and the chatId in 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

StatusMeaning
201Created. The body is { channel }.
400Body 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.
401Missing or invalid API key.
403insufficient_scope (sending-only key, or OAuth token without notifications:manage), or Notification channel limit reached (50 per team).
404Domain not found / Mailbox not found — the id is not in your team.
429More than 20 creates in a minute.

List channels#

All of the team's channels, newest first, including web_push channels. Not paginated.

GEThttps://api.poststack.dev/notification-channels
Request
curl 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 channel in Create a channel.

Status codes

StatusMeaning
200The channels (possibly an empty array).
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.

Get a channel#

One channel.

GEThttps://api.poststack.dev/notification-channels/:publicId
Request
curl 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 with 400 Invalid notification channel ID.

Response fields

channelobject
The channel.

Status codes

StatusMeaning
200The channel, as { channel }.
400Invalid notification channel ID.
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.
404Notification 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.

PUThttps://api.poststack.dev/notification-channels/:publicId
Request
curl -X PUT https://api.poststack.dev/notification-channels/nc_k3v9x2m7q8w4r1t6y5u0p2a9 \
  -H "Authorization: Bearer $POSTSTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "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 with 400 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 null to clear it.
mailboxIdinteger | null
Set a mailbox scope, or null to clear it.
enabledboolean
false disables the channel. true enables it and, after an auto-disable, resets consecutiveFailures, disabledAt and disabledReason.
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 for web_push channels.

Response fields

channelobject
The updated channel.

Status codes

StatusMeaning
200Updated (or unchanged when the body was empty), as { channel }.
400Validation 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.
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.
404Notification channel not found, or Domain / Mailbox not found.
429More 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.

DELETEhttps://api.poststack.dev/notification-channels/:publicId
Request
curl -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 with 400 Invalid notification channel ID.

Response fields

successboolean
Always true.

Status codes

StatusMeaning
200Deleted.
400Invalid notification channel ID.
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.
404Notification channel not found.
429More 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.

POSThttps://api.poststack.dev/notification-channels/:publicId/test
Request
curl -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 with 400 Invalid notification channel ID.

Response fields

successboolean
true only when the platform accepted the message; false when it rejected it or could not be reached — see statusCode and responseBody.
statusCodeinteger | null
The platform's HTTP status; null on a network error or timeout.
responseBodystring | null
The platform’s response (or the network error), first 500 characters.

Status codes

StatusMeaning
200The attempt was made; check statusCode.
400Invalid notification channel ID, or Channel is disabled.
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.
404Notification channel not found.
429More than 10 tests in a minute.

List deliveries#

The channel's delivery history, newest first, including test messages.

GEThttps://api.poststack.dev/notification-channels/:publicId/deliveries
Request
curl "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 with 400 Invalid notification channel ID.

Query parameters

pageintegerdefault 1
Page number, from 1.
per_pageintegerdefault 20
1–100.

Response fields

deliveries[].idinteger
Delivery id, used to replay it.
deliveries[].channelIdinteger
Internal channel id.
deliveries[].eventTypestring
The event, or test.
deliveries[].payloadobject
type, created_at, message (the formatted title / subtitle / body / link), and for real events raw (the webhook payload) and scope.
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; null while pending or after failing.
deliveries[].createdAtstring (ISO 8601)
When the event was dispatched.
metaobject
page, perPage, total, totalPages.

Status codes

StatusMeaning
200A page of deliveries.
400Invalid channel ID or query parameters.
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.
404Notification 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.

POSThttps://api.poststack.dev/notification-channels/:publicId/deliveries/:deliveryId/replay
Request
curl -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 with 400 Invalid notification channel ID.
deliveryIdintegerrequired
The id from List deliveries.

Response fields

successboolean
Always true: the delivery was queued.

Status codes

StatusMeaning
200Queued. Watch the delivery history for the result.
400Invalid channel ID or delivery id.
401Missing or invalid API key.
403insufficient_scope — a sending-only key, or an OAuth token without the scope this endpoint needs.
404Notification channel not found, or Delivery not found on this channel.
429More than 10 replays in a minute.

A replay of a disabled channel is queued but dropped when it runs — re-enable the channel first.

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.

Next steps

Was this page helpful?

Related

Notification channels overviewForward email events to Slack, Discord and Telegram.