Gareth Wilson Gareth Wilson

How to Secure and Verify Slack Webhooks with Hookdeck

Published


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 format v0=<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:

  1. Constructing a base string in the format: v0:{timestamp}:{raw_request_body}
  2. Computing an HMAC-SHA256 hash of this base string using your app's signing secret as the key
  3. 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:

  1. Go to api.slack.com/apps and select your app
  2. Navigate to Basic Information
  3. Scroll to App Credentials
  4. 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

  1. Extract the X-Slack-Request-Timestamp header and reject requests older than 5 minutes (prevents replay attacks)
  2. Construct the base string: v0:{timestamp}:{raw_request_body}
  3. Compute HMAC-SHA256 using your signing secret as the key
  4. Hex-encode the result and prepend v0=
  5. Compare your computed signature with X-Slack-Signature using 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
    
    
  1.     scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git
        
        
  2.   scoop install hookdeck
      
      
  1. Download the latest release's tar.gz file.

  2.     tar -xvf hookdeck_X.X.X_linux_x86_64.tar.gz
        
        
  3.   ./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

  1. Copy the Hookdeck Source URL from the CLI output
  2. In your Slack app settings, go to Event Subscriptions
  3. Toggle Enable Events to on
  4. Set the Request URL to your Hookdeck Source URL
  5. Slack will send a url_verification challenge — Hookdeck handles this automatically
  6. Select your event subscriptions and save

Step 5: Configure source verification

  1. Open the Hookdeck Dashboard
  2. Navigate to Connections and select your source
  3. Under Advanced Source Configuration, enable Source Authentication
  4. Select Slack from the list of platforms
  5. Enter your Slack Signing Secret
  6. Click Save

How Hookdeck verification works

When verification is enabled:

  1. Hookdeck receives the webhook from Slack
  2. Hookdeck validates the X-Slack-Signature against the payload using your signing secret
  3. Valid requests are forwarded to your endpoint with x-hookdeck-verified: true
  4. 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:

  1. Raw body usage: Ensure you're using the exact bytes received, not a parsed/stringified version
  2. Secret accuracy: Confirm you're using the Signing Secret (not the deprecated Verification Token)
  3. Timestamp inclusion: Ensure the timestamp is correctly extracted from the X-Slack-Request-Timestamp header
  4. Base string format: The format must be exactly v0:{timestamp}:{body} with colons as delimiters

Timestamp validation fails

  1. Server time sync: Ensure your server's clock is synchronized (use NTP)
  2. 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.