How to Secure and Verify Slack Webhooks with Hookdeck
Slack is the dominant workplace messaging platform, and its Events API delivers real-time workspace events to your application via HTTP callbacks. When events occur — such as messages being posted, reactions being added, or files being shared — Slack sends a JSON payload to your configured Request URL.
However, accepting these webhooks without proper verification leaves your application vulnerable to spoofed requests from malicious actors. In this guide, we'll show you how to secure and verify Slack webhooks manually and with Hookdeck, ensuring that every webhook your application processes genuinely originated from Slack. For a comprehensive overview of Slack webhook capabilities, see our guide to Slack webhooks.
Why webhook security matters
Webhooks are HTTP callbacks that notify your application when events occur. Without verification, an attacker could send fake webhook payloads to your endpoint, potentially triggering unauthorized actions like:
- Leaking sensitive workspace data to external systems
- Triggering automated workflows based on fabricated events
- Injecting malicious content into your integrations
- Impersonating users or bots within your application logic
Slack addresses this by signing every Events API webhook with an HMAC-SHA256 signature. By verifying this signature, you can be confident that webhooks are authentic and haven't been tampered with in transit.
Note: Slack incoming webhooks (used for posting messages into Slack) do not support signature verification — security relies entirely on keeping the webhook URL secret. This guide focuses on the Events API, where Slack sends events to your endpoint.
How Slack webhook signatures work
Slack uses HMAC-SHA256 signature verification for Events API webhooks. Every webhook request includes two headers for verification:
X-Slack-Signature— The computed HMAC signature in the formatv0=<hex_digest>X-Slack-Request-Timestamp— A Unix timestamp (in seconds) indicating when the request was sent
The signature generation process
Slack generates the signature by:
- Constructing a base string in the format:
v0:{timestamp}:{raw_request_body} - Computing an HMAC-SHA256 hash of this base string using your app's signing secret as the key
- Hex-encoding the result and prepending
v0=
To verify a webhook, you repeat this process and compare your computed signature against the X-Slack-Signature header value.
Getting your Slack signing secret
Before you can verify webhooks, you need to obtain your signing secret from Slack:
- Go to api.slack.com/apps and select your app
- Navigate to Basic Information
- Scroll to App Credentials
- Find the Signing Secret field — this is your webhook signing secret
Important: The signing secret is different from the deprecated Verification Token that appears in event payloads. Always use the signing secret for HMAC verification.
Verifying webhook signatures manually
Verification process overview
- Extract the
X-Slack-Request-Timestampheader and reject requests older than 5 minutes (prevents replay attacks) - Construct the base string:
v0:{timestamp}:{raw_request_body} - Compute HMAC-SHA256 using your signing secret as the key
- Hex-encode the result and prepend
v0= - Compare your computed signature with
X-Slack-Signatureusing a timing-safe comparison
Node.js verification example
const express = require("express");
const crypto = require("crypto");
const app = express();
const SLACK_SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET;
function verifySlackWebhook(rawBody, timestamp, signature) {
// Reject requests older than 5 minutes
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp, 10)) > 300) {
console.error("Webhook timestamp is outside tolerance window");
return false;
}
// Build the base string
const sigBaseString = `v0:${timestamp}:${rawBody}`;
// Compute HMAC-SHA256
const computedSignature =
"v0=" +
crypto
.createHmac("sha256", SLACK_SIGNING_SECRET)
.update(sigBaseString, "utf8")
.digest("hex");
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(signature)
);
}
app.post(
"/webhooks/slack",
express.raw({ type: "application/json" }),
(req, res) => {
const timestamp = req.headers["x-slack-request-timestamp"];
const signature = req.headers["x-slack-signature"];
const rawBody = req.body.toString();
// Handle URL verification challenge (required during setup)
const payload = JSON.parse(rawBody);
if (payload.type === "url_verification") {
return res.json({ challenge: payload.challenge });
}
if (!verifySlackWebhook(rawBody, timestamp, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Acknowledge immediately (Slack requires response within 3 seconds)
res.status(200).send("OK");
// Process event asynchronously
processEvent(payload).catch(console.error);
}
);
app.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});
Python verification example
import hmac
import hashlib
import time
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET")
def verify_slack_webhook(raw_body, timestamp, signature):
"""Verify a Slack webhook signature."""
# Reject requests older than 5 minutes
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
return False
# Build the base string
sig_basestring = f"v0:{timestamp}:{raw_body}"
# Compute HMAC-SHA256
computed_signature = "v0=" + hmac.new(
SLACK_SIGNING_SECRET.encode("utf-8"),
sig_basestring.encode("utf-8"),
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(computed_signature, signature)
@app.route("/webhooks/slack", methods=["POST"])
def handle_webhook():
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
signature = request.headers.get("X-Slack-Signature", "")
raw_body = request.get_data(as_text=True)
payload = request.get_json()
# Handle URL verification challenge
if payload.get("type") == "url_verification":
return jsonify({"challenge": payload["challenge"]})
if not verify_slack_webhook(raw_body, timestamp, signature):
abort(401, "Invalid signature")
# Acknowledge immediately
# Process asynchronously via task queue
process_event_async.delay(payload)
return "OK", 200
if __name__ == "__main__":
app.run(port=3000)
Critical security best practices
Use timing-safe comparisons
Never use standard equality operators (== or ===) to compare signatures. These can leak timing information that attackers exploit. Always use:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - Go:
subtle.ConstantTimeCompare()
Implement replay attack prevention
Check the X-Slack-Request-Timestamp header and reject webhooks where the timestamp differs from your server time by more than 5 minutes. This prevents attackers from replaying intercepted requests.
Preserve the raw request body
The HMAC signature is computed on the exact bytes Slack sends. Any parsing, reformatting, or middleware that modifies the body before verification will produce a different signature. Capture the raw body before JSON parsing.
Respond within 3 seconds
Slack's Events API requires your endpoint to respond with an HTTP 2xx within 3 seconds. If your handler takes longer, Slack marks the delivery as failed and retries — potentially causing duplicate processing. Acknowledge receipt immediately and defer processing to an asynchronous worker.
Handle the URL verification challenge
Before Slack delivers events, it verifies your endpoint with a url_verification challenge. Your endpoint must respond with the challenge value. Handle this before signature verification in your request handler, as challenge requests during initial setup may behave differently.
Simplifying verification with Hookdeck
Manually implementing and maintaining webhook verification across multiple providers can be complex and error-prone. Hookdeck provides a webhook gateway that handles verification automatically, allowing you to focus on processing events.
What is Hookdeck?
Hookdeck provides an event gateway that sits between webhook providers (like Slack) and your application. It provides:
- Automatic signature verification
- Event queuing and retry logic
- Request logging and debugging tools
- Local development tunneling
Setting up Slack webhooks with Hookdeck
Step 1: Install the Hookdeck CLI
npm install hookdeck-cli -g
yarn global add hookdeck-cli
brew install hookdeck/hookdeck/hookdeck
-
scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git -
scoop install hookdeck
-
Download the latest release's tar.gz file.
-
tar -xvf hookdeck_X.X.X_linux_x86_64.tar.gz -
./hookdeck
Step 2: Authenticate
hookdeck login
This opens your browser for authentication. If you don't have a Hookdeck account, you can create one during this step.
Step 3: Create a connection
hookdeck listen 3000 slack-source --path /webhooks/slack
This command:
- Creates a public URL for receiving webhooks
- Forwards events to
http://localhost:3000/webhooks/slack - Displays the Source URL to configure in Slack
Step 4: Configure Slack
- Copy the Hookdeck Source URL from the CLI output
- In your Slack app settings, go to Event Subscriptions
- Toggle Enable Events to on
- Set the Request URL to your Hookdeck Source URL
- Slack will send a
url_verificationchallenge — Hookdeck handles this automatically - Select your event subscriptions and save
Step 5: Configure source verification
- Open the Hookdeck Dashboard
- Navigate to Connections and select your source
- Under Advanced Source Configuration, enable Source Authentication
- Select Slack from the list of platforms
- Enter your Slack Signing Secret
- Click Save
How Hookdeck verification works
When verification is enabled:
- Hookdeck receives the webhook from Slack
- Hookdeck validates the
X-Slack-Signatureagainst the payload using your signing secret - Valid requests are forwarded to your endpoint with
x-hookdeck-verified: true - Invalid requests are rejected and logged as "Verification Failed"
This means your application can trust any request from Hookdeck without implementing its own Slack signature verification logic. You only need to implement Hookdeck's signature verification on your server.
Hookdeck also solves Slack's strict 3-second timeout requirement. Hookdeck acknowledges Slack's deliveries immediately, then delivers events to your endpoint with configurable timeouts and automatic retries — giving your handler more time to process events without triggering Slack's retry behavior.
Troubleshooting common issues
Signature mismatch
If signatures don't match, verify:
- Raw body usage: Ensure you're using the exact bytes received, not a parsed/stringified version
- Secret accuracy: Confirm you're using the Signing Secret (not the deprecated Verification Token)
- Timestamp inclusion: Ensure the timestamp is correctly extracted from the
X-Slack-Request-Timestampheader - Base string format: The format must be exactly
v0:{timestamp}:{body}with colons as delimiters
Timestamp validation fails
- Server time sync: Ensure your server's clock is synchronized (use NTP)
- Tolerance window: The standard tolerance is 5 minutes (300 seconds)
Retries causing duplicate processing
Slack retries events when your endpoint doesn't respond within 3 seconds. Use the event_id field to deduplicate events and check for the x-slack-retry-num header to identify retries.
Conclusion
Securing Slack webhooks requires verifying the HMAC-SHA256 signature on every incoming Events API request. While you can implement this verification manually, Hookdeck simplifies the process by providing automatic signature verification, event management, and development tools — while also solving Slack's strict 3-second timeout requirement, allowing you to focus on building your integration rather than infrastructure.