Building Slack Notifications with Webhooks: A Practical Guide
Turn any webhook event into a Slack notification. Learn how to build a webhook-to-Slack pipeline with formatted messages, error alerts, and smart routing.
HookWatch Team
February 22, 2026
Webhooks fire when things happen. Slack is where your team lives. Connecting the two means your team knows about critical events the moment they occur — new orders, failed payments, deployment completions, security alerts.
This guide walks through building a webhook-to-Slack pipeline from scratch.
Setting Up Slack Incoming Webhooks
First, create a Slack app with an incoming webhook:
- Go to api.slack.com/apps and create a new app
- Under Incoming Webhooks, toggle it on
- Click Add New Webhook to Workspace
- Select the channel and authorize
- Copy the webhook URL
Your Slack webhook URL looks like this:
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
Test it with a simple curl:
curl -X POST -H 'Content-Type: application/json' \
--data '{"text": "Hello from webhooks!"}' \
https://hooks.slack.com/services/T00/B00/XXX
The Architecture
Webhook Provider → HookWatch → Your Server → Slack
(Stripe) (proxy) (formatter) (channel)
HookWatch receives the webhook, ensures delivery, and forwards it to your server. Your server formats the event into a Slack message and sends it to the appropriate channel.
Why not send directly to Slack? Because:
- You need to format raw webhook payloads into readable messages
- You want to route different events to different channels
- You need filtering to avoid notification fatigue
- You want retry logic if Slack is temporarily down
Building the Webhook Handler
const express = require('express');
const app = express();
app.use(express.json());
const SLACK_CHANNELS = {
payments: 'https://hooks.slack.com/services/T00/B01/payments',
orders: 'https://hooks.slack.com/services/T00/B02/orders',
alerts: 'https://hooks.slack.com/services/T00/B03/alerts'
};
app.post('/webhooks/handler', async (req, res) => {
const event = req.body;
try {
const slackMessage = formatEvent(event);
const channel = routeEvent(event);
await sendToSlack(channel, slackMessage);
res.status(200).json({ processed: true });
} catch (error) {
console.error('Failed to process webhook:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
Formatting Messages with Block Kit
Plain text messages get ignored. Slack's Block Kit lets you create structured, visually distinct notifications:
function formatPaymentEvent(event) {
const { amount, currency, customer_email, status } = event.data;
const emoji = status === 'succeeded' ? ':white_check_mark:' : ':x:';
return {
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${emoji} Payment ${status}`
}
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Amount:*\n${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`
},
{
type: 'mrkdwn',
text: `*Customer:*\n${customer_email}`
}
]
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Event ID: ${event.id} | ${new Date().toISOString()}`
}
]
}
]
};
}
Smart Routing
Not every event belongs in every channel. Route based on event type and severity:
function routeEvent(event) {
const type = event.type || event.event;
// Payment events
if (type.startsWith('payment.') || type.startsWith('invoice.')) {
return SLACK_CHANNELS.payments;
}
// Order events
if (type.startsWith('order.') || type.startsWith('checkout.')) {
return SLACK_CHANNELS.orders;
}
// Failures go to alerts
if (type.includes('failed') || type.includes('error')) {
return SLACK_CHANNELS.alerts;
}
// Default channel
return SLACK_CHANNELS.alerts;
}
Filtering Noise
Your team doesn't need a notification for every single event. Filter by severity and frequency:
const NOTIFY_EVENTS = new Set([
'payment.succeeded',
'payment.failed',
'order.completed',
'subscription.canceled',
'charge.disputed'
]);
function shouldNotify(event) {
const type = event.type || event.event;
// Always notify for high-priority events
if (NOTIFY_EVENTS.has(type)) return true;
// Notify for failures regardless of type
if (event.data?.status === 'failed') return true;
// Skip everything else
return false;
}
app.post('/webhooks/handler', async (req, res) => {
const event = req.body;
if (!shouldNotify(event)) {
return res.status(200).json({ skipped: true });
}
// ... send to Slack
});
Error Handling
Slack occasionally returns 429 (rate limited) or 5xx errors. Handle these gracefully:
async function sendToSlack(webhookUrl, message, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});
if (response.ok) return;
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 5;
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (response.status >= 500 && attempt < retries) {
await new Promise(r => setTimeout(r, attempt * 2000));
continue;
}
throw new Error(
`Slack API error: ${response.status} ${response.statusText}`
);
}
}
Using HookWatch Alerts Instead
If you don't need custom formatting, HookWatch has built-in Slack alert integration. Configure it from the dashboard:
- Go to Alerts in the HookWatch dashboard
- Click Add Alert
- Select Slack as the channel
- Paste your Slack webhook URL
- Set conditions (e.g., "alert when failure rate exceeds 5%")
This gives you failure notifications without writing any code. Use the custom approach above when you need formatted messages for business events (new orders, payments, signups).
Production Checklist
Before going live with webhook-to-Slack notifications:
- Rate limit your messages — batch events if volume is high
- Include event IDs — makes it easy to look up details in HookWatch
- Add action buttons — link to dashboards, retry pages, or customer profiles
- Use threads — group related events (e.g., all events for one order)
- Monitor delivery — HookWatch tracks whether your handler responded successfully
- Test with the Sandbox — send sample events through HookWatch's Sandbox to verify formatting before going live