Incoming Webhooks (send to Slack) and Events API (receive from Slack) — setup, verification, and code examples.
Last updated March 2026. Covers Slack API v2 and Bolt SDK.
Slack has two distinct webhook systems that work in opposite directions. Understanding which one you need is the first step.
You POST a JSON message to a Slack-provided URL, and it appears in a channel. Used for notifications, alerts, deploy messages, and CI/CD updates.
// Send a message to Slack via Incoming Webhook
const webhookUrl = 'https://hooks.slack.com/services/T.../B.../xxx';
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: 'Deployment succeeded!',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Deployment succeeded* :white_check_mark:\nService api-v2 deployed to production.'
}
}
]
})
});
Slack sends HTTP POST requests to YOUR server when events happen — messages posted, reactions added, users joining channels. Used for bots, workflow automation, and integrations.
Common Events API event types:
Every Events API request from Slack is signed. You must verify the signature to ensure the request is genuinely from Slack, not an attacker.
Slack uses X-Slack-Signature (HMAC-SHA256) and X-Slack-Request-Timestamp headers:
const crypto = require('crypto');
function verifySlackSignature(req, signingSecret) {
const timestamp = req.headers['x-slack-request-timestamp'];
const slackSig = req.headers['x-slack-signature'];
// Reject requests older than 5 minutes (prevents replay attacks)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
throw new Error('Request timestamp too old');
}
// Compute expected signature
const sigBaseString = 'v0:' + timestamp + ':' + req.rawBody;
const mySignature = 'v0=' + crypto
.createHmac('sha256', signingSecret)
.update(sigBaseString)
.digest('hex');
// Constant-time comparison to prevent timing attacks
if (!crypto.timingSafeEqual(
Buffer.from(mySignature),
Buffer.from(slackSig)
)) {
throw new Error('Invalid signature');
}
return true;
}
// Express middleware
app.post('/slack/events', express.raw({ type: '*/*' }), (req, res) => {
req.rawBody = req.body.toString();
try {
verifySlackSignature(req, process.env.SLACK_SIGNING_SECRET);
} catch (err) {
return res.status(401).json({ error: err.message });
}
const event = JSON.parse(req.rawBody);
// Handle URL verification challenge
if (event.type === 'url_verification') {
return res.json({ challenge: event.challenge });
}
// Process the event
console.log('Event:', event.event.type, event.event);
res.status(200).send();
});
Key details: use the raw request body for signature computation (not parsed JSON). Check the timestamp is within 5 minutes to prevent replay attacks. Use constant-time comparison (timingSafeEqual) to prevent timing side-channel attacks.
To receive Events API webhooks, configure your Slack App:
message.channels, app_mention).For Incoming Webhooks, go to Incoming Webhooks in your app settings, activate it, and click "Add New Webhook to Workspace" to select a channel.
During development you need a public URL for Slack to reach. The SDNBros Webhook Tester gives you one instantly:
| Error | Cause | Fix |
|---|---|---|
| url_verification failed | Your endpoint did not respond with the challenge value, or responded too slowly (>3s) | Return { "challenge": event.challenge } immediately. Do not process anything else during verification. |
| Invalid signature | Using parsed JSON body instead of raw body, or wrong signing secret | Use raw request body for HMAC. Find your Signing Secret in App Settings > Basic Information (not the bot token). |
| Request timestamp too old | Your server clock is more than 5 minutes out of sync with Slack | Sync your server clock with NTP. Check if a proxy is caching/delaying requests. |
| dispatch_failed | Slack could not reach your endpoint or it returned non-2xx within 3 seconds | Ensure your URL is publicly accessible over HTTPS. Return 200 quickly and process events asynchronously. |
| Duplicate events | Slack retries if your endpoint does not respond within 3 seconds | Return 200 immediately. Use the event_id field to deduplicate. Process events in a background queue. |
Create a public endpoint and inspect Slack events and URL-verification challenges.
Open Webhook Tester Monitor in ProductionRelated guides: What is a Webhook? · Webhook vs API · Webhook Security · Retry Strategies · Stripe · GitHub · Shopify
More: What is a Webhook? | Webhook Security Guide | Stripe Webhooks | Retry Strategies