Slack request signature verification failing (X-Slack-Signature)
Slack signs Events API calls, slash commands and interactive payloads with your app's Signing Secret. The signature is v0= followed by the hex HMAC-SHA256 of the string v0:{X-Slack-Request-Timestamp}:{raw body}.
Open the Slack webhook tester →
The check
import crypto from 'node:crypto';
function verifySlack(rawBody, headers, signingSecret) {
const ts = headers['x-slack-request-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 60 * 5) return false; // replay window
const expected = 'v0=' + crypto.createHmac('sha256', signingSecret).update(`v0:${ts}:${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(headers['x-slack-signature'] || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hashlib, hmac, time
def verify_slack(raw_body: bytes, headers, signing_secret: str) -> bool:
ts = headers['X-Slack-Request-Timestamp']
if abs(time.time() - int(ts)) > 60 * 5:
return False
base = b'v0:' + ts.encode() + b':' + raw_body
expected = 'v0=' + hmac.new(signing_secret.encode(), base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers.get('X-Slack-Signature', ''))Cause 1: slash commands and interactivity are form-encoded
Slash commands and interactive components arrive as application/x-www-form-urlencoded. If a form parser runs first, the raw string is gone. Keep the raw body for these routes and parse it only after the check.
Cause 2: the wrong secret
Use the Signing Secret from the app's Basic Information page. The bot token (xoxb-…) and the legacy verification token are not signing keys.
Cause 3: an old timestamp
Slack recommends rejecting requests whose timestamp is more than five minutes away from your clock, to block replays. A server with a drifting clock rejects valid requests; sync it with NTP.
Frequently asked questions
Should I verify requests from Slack's retries too?
Yes. Retries are signed like the first delivery and carry an X-Slack-Retry-Num header. Verify them the same way, then deduplicate on the event_id so the work runs once.
Can I see what Slack sends?
Put a Webhook Toolkit URL as the Request URL: the inspector shows the raw form body and both headers. To answer the challenge from your own code, use the Relay tunnel.