All Webhook Errors

Slack Webhook Signature Mismatch — Causes & Fixes

Slack signs requests with HMAC-SHA256 in X-Slack-Signature, with a timestamp in X-Slack-Request-Timestamp outside the signed material but used to construct the signed string. Mismatches usually come from one of four issues.

How Slack's Signature Works

The signed input is the literal string:

v0:<X-Slack-Request-Timestamp>:<raw_request_body>

HMAC-SHA256 it with your signing secret, hex-encode, prefix with v0=. The result is what's in X-Slack-Signature.

Root Causes

1. Using the bot token instead of the signing secret

Slack apps have multiple secrets, and they're easy to confuse:

  • Signing Secret (the one you want for webhook verification) — under "Basic Information" in your Slack app's settings.
  • Bot User OAuth Token (xoxb-...) — for calling Slack APIs as the bot.
  • App-Level Token (xapp-...) — for socket mode.

Using the bot token to verify HMAC produces 100% mismatches.

2. Forgetting the v0 prefix

The signed string starts with literal v0:, and the result is prefixed with v0=. Both are mandatory.

3. Body was parsed before signature check

Slack's body is form-encoded by default (sometimes JSON for Events API). Either way, you need the raw bytes for HMAC computation. Don't let express.urlencoded() or express.json() run first.

4. Timestamp drift

Slack recommends rejecting requests with timestamp older than 5 minutes — both as replay defense and because Slack itself rejects deliveries older than that. Your check should match.

Fix It

import express from 'express'
import crypto from 'node:crypto'

const app = express()
const signingSecret = process.env.SLACK_SIGNING_SECRET

app.post('/webhooks/slack',
  express.raw({ type: '*/*' }),  // works for both form-encoded and JSON
  (req, res) => {
    const ts = req.headers['x-slack-request-timestamp']
    const sig = req.headers['x-slack-signature']
    if (!ts || !sig) return res.status(401).send('missing signature')

    // Reject if timestamp is older than 5 minutes (replay defense)
    const fiveMinAgo = Math.floor(Date.now() / 1000) - 60 * 5
    if (parseInt(ts, 10) < fiveMinAgo) {
      return res.status(401).send('timestamp too old')
    }

    const baseString = \`v0:\${ts}:\${req.body.toString()}\`
    const expected = 'v0=' + crypto
      .createHmac('sha256', signingSecret)
      .update(baseString)
      .digest('hex')

    const sigBuf = Buffer.from(sig, 'utf-8')
    const expBuf = Buffer.from(expected, 'utf-8')
    if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
      return res.status(401).send('invalid signature')
    }

    // Parse the body now that it's verified
    const params = new URLSearchParams(req.body.toString())
    res.status(200).send('ok')
  }
)

Slack-Specific Gotchas

  • Slack sends both slash commands (form-encoded) and Events API (JSON) — the same signing scheme works for both, as long as you compute HMAC over the raw body whatever its format.
  • Slack requires acknowledgment within 3 seconds — extremely tight. Always queue async; never run business logic synchronously.
  • The URL verification challenge (Events API initial setup) sends a type=url_verification payload that you must echo back the challenge field for. Treat it as a special-case before your normal handler logic.
  • Slack does not retry 4xx responses. A 401 is terminal — no retries.

How to Reproduce

In your Slack app config: "Event Subscriptions" → "Verify URL" button. Slack fires a signed verification challenge. If your handler rejects it, the verification setup blocks. Use the captured request body and headers in WebhookWhisper to debug locally.

Frequently Asked Questions

What's the difference between Slack signing secret and bot token?

Signing secret verifies that requests came from Slack. Bot token authorizes you to call Slack APIs. Different purposes, different secrets, both required for a full integration.

Why is Slack's timeout so short (3 seconds)?

Slack expects all interactive flows to feel instant — a slash command response that takes 5 seconds breaks the UX. Always ack immediately, do work async, then post follow-up messages via the response URL or Web API.

Can I use the same handler for slash commands and Events API?

Yes, the signing scheme is identical. The body format differs (form-encoded vs JSON) — branch on Content-Type after verification.

Debug This Error in Real Time

WebhookWhisper captures every webhook request with full headers, body, and timing — so you can see exactly what the provider sent and reproduce the error instantly.

Start Debugging Free
Slack Webhook Signature Mismatch — Causes & Fixes (2026) | WebhookWhisper