WebhookToolkit
Slack

Slack webhook tester

"Slack webhook" means two different things, and most tools only cover one. This page covers both: the outbound Incoming Webhook you POST messages to, and the inbound signed request Slack sends your app (Events API / Slash commands) that you must verify with X-Slack-Signature.

The interactive tool below validates a v0 signature entirely in your browser — paste the header, secret, timestamp and raw body, get PASS/FAIL. For sending to an Incoming Webhook, browsers are blocked by CORS (explained below), so use the copy-paste curl / Node.

Validate an X-Slack-Signature

Pick Slack, paste the v0=… header, your Signing Secret, the X-Slack-Request-Timestamp, and the raw body. If it fails, the validator tells you which mutation (trimmed secret, stray newline) would have made it pass.

Tout s'exécute dans votre navigateur avec l'API Web Crypto. Votre secret de signature n'est jamais envoyé à nos serveurs — vérifiez-le dans l'onglet Réseau.

Send to an Incoming Webhook

An Incoming Webhook takes a simple JSON body — a text string, or a blocks array for Block Kit. On success Slack returns HTTP 200 with the literal body ok. No signature, no auth header — the secret is the URL.

curl — plain text
curl -X POST -H 'Content-type: application/json' \
  --data '{"text":"Deploy finished :rocket:"}' \
  https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXX
curl — Block Kit
curl -X POST -H 'Content-type: application/json' \
  --data '{
    "blocks": [
      { "type": "header", "text": { "type": "plain_text", "text": "Build #1287" } },
      { "type": "section", "text": { "type": "mrkdwn", "text": "*env:* production\n*commit:* a1b2c3d" } }
    ]
  }' \
  https://hooks.slack.com/services/T0.../B0.../XXXX
Node (fetch)
const res = await fetch(process.env.SLACK_WEBHOOK_URL, {
  method: "POST",
  headers: { "Content-type": "application/json" },
  body: JSON.stringify({ text: "Deploy finished :rocket:" }),
});
const body = await res.text(); // "ok" on success

Why there is no "Send" button here

A JSON POST is not a CORS-simple request, so the browser first sends an OPTIONS preflight. hooks.slack.com answers that preflight without an Access-Control-Allow-Methods header, so Chrome/Firefox reject the real POST before it is ever sent. That is a property of Slack's endpoint, not a bug you can work around from a static page — you need a server, curl, or a local runtime. Discord, by contrast, does allow it, which is why the Discord tester has a live send button and this one doesn't.

How Slack signs the requests it sends you

For the Events API and Slash commands, Slack signs every request. Your handler recomputes the same HMAC and compares — in constant time. The basestring is the part people get wrong: it is v0: + timestamp + : + the raw body.

The two headers Slack sends
X-Slack-Request-Timestamp: 1531420618
X-Slack-Signature: v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503
StepValue
basestringv0:{X-Slack-Request-Timestamp}:{raw body}
algorithmHMAC-SHA256, keyed with the Signing Secret
encodinglowercase hex, prefixed with v0=
compareconstant-time against X-Slack-Signature
replay guardreject if |now - timestamp| > 300s (5 min)

The Signing Secret lives in your app config under Basic Information → App Credentials → Signing Secret — it is not the Bot Token and not the Incoming Webhook URL.

Verify it in your handler

Node (Express, raw body)
import crypto from "crypto";

function verifySlack(req) {
  const ts = req.headers["x-slack-request-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay guard
  const base = `v0:${ts}:${req.rawBody}`; // rawBody = exact bytes, not JSON.stringify
  const expected =
    "v0=" + crypto.createHmac("sha256", process.env.SLACK_SIGNING_SECRET).update(base).digest("hex");
  const got = req.headers["x-slack-signature"];
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}

Common failures

What you seeCauseFix
invalid_payloadMalformed JSON or no text/blocksValidate the JSON; include text or a blocks array
channel_not_foundTarget channel deleted / app can't postReconnect the webhook to a live channel
no_service / no_active_hooksWebhook revoked or app removedRegenerate the Incoming Webhook URL
signature mismatchRe-serialized body or trimmed/wrong secretHMAC the RAW body; check the Signing Secret has no stray whitespace
signature valid but rejectedTimestamp outside the 5-min windowReject replays, but check server clock skew first

Related tools

Debugging the outbound signature (signing a payload so you can test your handler locally) is the job of the Signer. The generic multi-provider validator is one step over.

Webhook signature validatorSign a test payloadDiscord webhook tester

Questions fréquentes

Can I POST to a Slack Incoming Webhook from the browser?

No — and that trips up a lot of people. hooks.slack.com does not answer the CORS preflight for a JSON POST (it returns no Access-Control-Allow-Methods), so a browser fetch is blocked before it leaves the page. Send from curl, a server, or a runtime like Node/Python instead. That is why the interactive tool here is the signature validator (pure client-side crypto) rather than a fake send button.

How is the X-Slack-Signature computed?

v0=HMAC-SHA256 over the string "v0:{timestamp}:{raw_body}", keyed with your app's Signing Secret, hex-encoded, prefixed with "v0=". The timestamp is the X-Slack-Request-Timestamp header value, and the body is the exact raw request body — not a re-serialized JSON.

What is the 5-minute replay rule?

Before checking the signature, Slack recommends you reject any request whose X-Slack-Request-Timestamp is more than 300 seconds (5 minutes) away from now. This stops an attacker from capturing a valid signed request and replaying it later. A captured request replayed after 5 minutes should be dropped even if the signature is mathematically correct.

What does no_service or no_active_hooks mean?

The Incoming Webhook was revoked or the app was removed from the workspace — the URL is dead and must be regenerated. invalid_payload means malformed JSON or missing text/blocks. channel_not_found means the target channel was deleted or the app can't post there.

Incoming Webhook vs Events API vs Slash command — which URL?

Incoming Webhook: YOU POST to a hooks.slack.com URL, no signature needed. Events API: SLACK POSTs to YOUR Request URL and you must verify X-Slack-Signature and answer the url_verification challenge. Slash commands: SLACK POSTs application/x-www-form-urlencoded to YOUR command URL; you reply inline or via the response_url. Only the last two require signature verification.

Slack Webhook Tester — send to an Incoming Webhook & verify X-Slack-Signature · Webhook Toolkit