Send Slack notifications from your app with one API call.
A Slack notification from your product is a message to a person or a channel: a deploy finished, a payment failed, a teammate was mentioned. Notifly sends it from a workflow, so the same trigger can also reach email, SMS, push and the in-app inbox.
Notifly sends as a Slack app you own. The Slack integration guide covers its credentials, redirect URL and scopes; this page covers what comes after: choosing a delivery route, the chat step, and the code that sends.
Two ways to reach Slack
Both routes send through your Slack app and an active Slack integration in your Notifly environment. They differ in where the message goes and what you store.
| Route | Where the message goes | What you store | Good for |
|---|---|---|---|
| Incoming webhook | The channel the webhook URL was created for | The webhook URL, saved on the subscriber | Alerts to a fixed channel, the shortest setup |
| Your Slack app's bot | A Slack user or a channel the bot can post to | A workspace connection, created when a user approves your app | Direct messages to individual users |
How a chat step reaches Slack
- In the workflow editor, add a Chat step and write the message. It can use the trigger's payload, such as
{{payload.service}}. - Trigger the workflow for a subscriber. Before the step runs, Notifly checks that subscriber's preferences for the chat channel, and an opted-out step is skipped with the reason recorded.
- Notifly finds where that subscriber's Slack messages go, through the environment's active Slack integration: a Slack user or channel from a workspace connection, or the webhook URL saved on the subscriber.
- It sends with
chat.postMessageand the bot token, or posts to the webhook, and records Slack's answer in the activity feed. A subscriber with no Slack destination gets no message, and the activity feed says so.
Put a digest step ahead of the chat step when a burst of events should arrive as one Slack message.
Send to an incoming webhook
- In your Slack app, turn on Incoming Webhooks, add a webhook to the channel and copy its URL.
- In the dashboard, open Integration Store, choose Connect Provider, open the Chat tab and connect Slack with your app's credentials, as the Slack integration guide shows.
- Create each subscriber who should receive messages there and save the webhook URL on them. Saving credentials for a subscriber id Notifly has not seen is rejected.
- Trigger the workflow that has the Chat step.
// npm install @notiflyio/api@0.1.26
import { Notifly } from "@notiflyio/api";
const secretKey = process.env.NOTIFLY_SECRET_KEY;
if (!secretKey) throw new Error("NOTIFLY_SECRET_KEY is not set");
const notifly = new Notifly({ security: { secretKey } });
// Once per subscriber: create them (saving credentials for an unknown subscriber id is rejected),
// then save the incoming webhook their Slack messages go to.
await notifly.subscribers.create({ subscriberId: "user_123" });
await notifly.subscribers.credentials.update(
{
providerId: "slack",
credentials: { webhookUrl: "https://hooks.slack.com/services/T0000000/B0000000/XXXXXXXXXXXXXXXXXXXXXXXX" },
},
"user_123",
);
// Any time after that: trigger the workflow that has a Chat step.
await notifly.trigger({
workflowId: "deploy-finished",
to: "user_123",
payload: { service: "billing-api", status: "succeeded" },
});# Once per subscriber: create them, then save their incoming webhook URL.
curl -X POST https://api.notifly.io/v1/subscribers \
-H "Authorization: ApiKey $NOTIFLY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"subscriberId":"user_123"}'
curl -X PUT https://api.notifly.io/v1/subscribers/user_123/credentials \
-H "Authorization: ApiKey $NOTIFLY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"providerId":"slack","credentials":{"webhookUrl":"https://hooks.slack.com/services/T0000000/B0000000/XXXXXXXXXXXXXXXXXXXXXXXX"}}'
# Any time after that: trigger the workflow.
curl -X POST https://api.notifly.io/v1/events/trigger \
-H "Authorization: ApiKey $NOTIFLY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"deploy-finished","to":{"subscriberId":"user_123"},"payload":{"service":"billing-api","status":"succeeded"}}'Notifly checks that the webhook URL is a safe public address before it posts, and a blocked URL fails the send. See the REST API docs for authentication and environments.
Send as your Slack app's bot
For direct messages, let each user approve your Slack app once. Your backend creates a connect link, the user approves it in Slack, and Notifly stores the workspace connection.
// npm install @notiflyio/api@0.1.26
import { Notifly } from "@notiflyio/api";
const secretKey = process.env.NOTIFLY_SECRET_KEY;
if (!secretKey) throw new Error("NOTIFLY_SECRET_KEY is not set");
const notifly = new Notifly({ security: { secretKey } });
// Create a connect link for the signed-in user (valid for 5 minutes).
const { result } = await notifly.integrations.generateConnectOAuthUrl({
integrationIdentifier: "slack-workspace", // the Identifier of your Slack integration
subscriberId: "user_123",
autoLinkUser: true, // also link the Slack user who approves the install
});
// Redirect the user to result.url and let them approve your app in Slack.With autoLinkUser the Slack user who approves is linked to that subscriber, so a later trigger addressed to user_123 reaches them in Slack. The install asks for the bot scopes chat:write, chat:write.public, channels:read, groups:read, users:read and users:read.email unless you pass your own scope list.
One workflow, many Slack destinations
Destinations belong to subscribers, not to the workflow. Save a different webhook URL for each subscriber, or connect each user's Slack, and one workflow reaches every one of them. A trigger addressed to a list of subscribers or to a topic fans out the same way, as Workflows describes.
Message options and failures
Anything under overrides.providers.slack is merged into the message Notifly sends, converted to Slack's snake_case: unfurlLinks: false arrives as unfurl_links.
await notifly.trigger({
workflowId: "deploy-finished",
to: "user_123",
payload: { service: "billing-api", status: "succeeded" },
overrides: { providers: { slack: { unfurlLinks: false } } },
});When Slack refuses a bot message, the step fails with the error Slack returned, such as Slack API Error: channel_not_found. A webhook that Slack answers with an error status, such as a revoked URL, fails with Chat webhook URL blocked: Request failed with status 404. Either way the activity feed records it.
Frequently asked questions
Do I need my own Slack app to send Slack notifications?
Yes. Notifly sends as a Slack app you own, and the dashboard's Slack integration asks for its App ID, Client ID, Client Secret and Signing Secret. Both the webhook route and the bot route run through that integration.
Should I use an incoming webhook or the bot?
Use a webhook for alerts to one fixed channel: you save a URL on the subscriber and nothing else. Use the bot route when messages should go to individual users, because each user approves your Slack app once and Notifly keeps the workspace connection.
Can one workflow message many people or channels?
Yes. Each subscriber carries their own Slack destination, either a saved webhook URL or a linked Slack user, so a trigger addressed to several subscribers or to a topic sends each of them a message at their own destination.
How long does a connect link stay valid?
Five minutes. The link's state is signed and time-stamped, so Notifly refuses an expired or altered link when Slack sends the user back. Create the link when the user clicks Connect, not ahead of time.
What happens when Slack rejects a message?
The step fails and the activity feed records why. A bot send fails with the error Slack returned, for example Slack API Error: channel_not_found. A webhook that Slack answers with an error status, such as a revoked URL, fails with Chat webhook URL blocked: Request failed with status 404, and a webhook reply that is not ok fails with Slack Webhook Error.
Can a webhook URL point at any address?
No. Notifly checks that the URL is a safe public address before posting, and a blocked URL fails the send with a Chat webhook URL blocked error instead of reaching an internal host. That prefix also starts the error for a webhook Slack answers with an error status.
Can I change the Slack message options per trigger?
Yes. Anything under overrides.providers.slack in the trigger is merged into the message and converted to Slack's snake_case, so unfurlLinks: false arrives as unfurl_links.
One workflow for in-app, email, SMS, push and chat.
The free plan includes 10,000 events per month, no card needed.