Skip to content
TryWebhook
New URL

Messaging · Free · No signup

Test and debug Slack webhooks and Events API

Capture Slack's url_verification handshake and real Events API deliveries on a throwaway URL, and read the timestamp and v0 signature that a Slack app must verify inside three seconds.

No signup · No email · Ready in about a second

At a glance

Slack webhooks in brief

Category
Messaging
Where to configure
api.slack.com/apps → your app → Event Subscriptions → Request URL
Signature header
X-Slack-Signature
Secret
Signing secret (Basic Information → App Credentials)
Retries
Retried three times with backoff — immediately, then about a minute, then about five. Sustained failures disable the subscription and Slack emails the app owner.
Response deadline
3 seconds. The tightest deadline of any provider here.
Treated as success
Any 2xx. The body is ignored except for the url_verification handshake, which must echo the challenge.

Cross-checked against Slack's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Slack actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}126 B

The handshake Slack sends first; you must echo `challenge`.

2 headers
user-agent
Slackbot 1.0 (+https://api.slack.com/robots)
x-slack-request-timestamp
1750000000
Body · application/json · 1 line
{"token":"FakeVerificationToken","challenge":"3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P","type":"url_verification"}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Slack signature

Signed payload

The literal `v0=` followed by a hex HMAC-SHA256 of the string `v0:{X-Slack-Request-Timestamp}:{raw body}`, keyed with the app's signing secret.

Implemented as: HMAC-SHA256 over `v0:{timestamp}:{raw body}`, hex, prefixed `v0=`.

Built in. Open any captured request, switch to the Signature tab, pick Slack, and paste your signing secret (basic information → app credentials). The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
url_verificationSent once when you save a Request URL. You must respond with the `challenge` value or Slack refuses to save the URL at all.
message.channelsA message was posted in a public channel your app is in. Also fires for edits and deletions, distinguished by `subtype`.
app_mentionSomeone typed your app's handle. The event most bots are actually built around, and it is delivered even when `message.channels` is not subscribed.
reaction_addedAn emoji reaction was added. Carries the item reference rather than the message itself — you fetch the message if you need it.
app_uninstalledYour app was removed from a workspace. Delete the tokens; they are already dead.

Capturing the handshake

Create a URL above, then open api.slack.com/apps → your app → Event Subscriptions and paste it into Request URL. Slack immediately POSTs the handshake:

{"token":"...","challenge":"3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P","type":"url_verification"}

It will appear in the list here within a second — and Slack will then show a red “Your URL didn’t respond with the value of the challenge parameter” error, because a capture bin returns its own acknowledgement rather than your challenge string. That is the expected outcome, and it is still useful: it proves Slack can reach the URL, and it shows you the exact shape you have to answer.

The handler Slack is waiting for is three lines:

if (body.type === 'url_verification') {
  return new Response(body.challenge, { headers: { 'content-type': 'text/plain' } });
}

Once your own endpoint does that, point Slack at it and use this page for the deliveries afterwards.

Verifying the v0 signature

Two headers arrive together:

X-Slack-Request-Timestamp: 1750000000
X-Slack-Signature: v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503

The signed string is built from three pieces joined by colons:

v0:1750000000:{"token":"...","challenge":"...","type":"url_verification"}

HMAC-SHA256 that with your signing secret, hex-encode it, prefix v0=, and compare in constant time against the header. In the Signature tab above, pick Slack and paste the secret; the timestamp comes from the captured header.

The timestamp check is not optional

Slack’s signature contains a timestamp but does not expire. The expiry is your job:

const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 60 * 5) return new Response('stale', { status: 400 });

Do this before the HMAC, not after. It is cheaper and it closes the replay window that the signature alone leaves open.

Designing for three seconds

Slack’s deadline shapes the whole architecture of a Slack app. There is no way to make a three-second budget cover a database write, an LLM call, and a chat.postMessage. The pattern that works:

  1. Verify the signature and the timestamp.
  2. Return 200 with an empty body.
  3. Do the work in a queue, a background task, or a waitUntil-style continuation.
  4. Reply through response_url (valid for 30 minutes) or chat.postMessage.

For slash commands you can return a short acknowledgement message in step 2 and replace it later — that is what response_type: "ephemeral" plus a follow-up to response_url is for.

Reading event bodies

Real events arrive wrapped, not bare:

{
  "type": "event_callback",
  "team_id": "T0000000000",
  "api_app_id": "A0000000000",
  "event_id": "Ev0000000000",
  "event_time": 1750000000,
  "event": { "type": "app_mention", "user": "U0000", "text": "<@U0BOT> deploy", "channel": "C0000", "ts": "1750000000.000100" }
}

The outer type is always event_callback; the one you switch on is event.type. event_id is your idempotency key, and event.ts is both the message timestamp and its identifier — you need it to reply in a thread.

One thing that catches every new Slack bot: your own bot’s messages are events too. Check event.bot_id and return early, or the first thing your bot says will trigger it again.

Failure modes

Where Slack integrations usually break

Three seconds, and Slack means it

Slack retries anything slower than three seconds and shows users a timeout warning for slash commands. Every non-trivial Slack app answers immediately and does the work afterwards, replying later through `response_url` or `chat.postMessage`.

The handshake blocks setup

You cannot save a Request URL until something at that URL echoes the `challenge` field back. A capture bin cannot do that, so use this page to read the handshake and confirm it arrives, then point Slack at your own endpoint once it can answer.

The signed string has three parts

`v0:{timestamp}:{body}` — with the literal `v0` and two colons. Forgetting the version prefix, or using the body alone, produces a digest that never matches. The colons are part of the string, not separators in a diagram.

Reject old timestamps

Slack's own guidance is to reject any request whose `X-Slack-Request-Timestamp` is more than five minutes old. Without that check, one captured request is a permanent replay token, because the signature itself never expires.

Retries look like new events

A retry carries `X-Slack-Retry-Num` and `X-Slack-Retry-Reason`. Bots that post a reply per delivery send the same message three times when the first two replies were slow. Deduplicate on `event_id`, or check the retry header and skip.

Answers

Slack webhook questions

Why will Slack not save my Request URL?+

Because the handshake is not being answered. Slack POSTs a `url_verification` body containing a `challenge` string and requires that exact string back, as plain text or as JSON, within three seconds. Capture it here to confirm Slack is reaching you, then implement the echo on your own endpoint.

Can I verify the Slack signature here?+

Yes. Choose Slack in the Signature tab and paste your signing secret. The `X-Slack-Request-Timestamp` header from the captured request is folded into the signed string automatically, which is the part hand-rolled verifiers usually get wrong.

What is the difference between an incoming webhook and the Events API?+

An incoming webhook is a URL Slack gives you to post *into* Slack. The Events API is the reverse — Slack posting out to your server when something happens. This page is about the second one; the first needs no verification because you are the sender.

Why am I getting the same event three times?+

Your endpoint is taking longer than three seconds, so Slack is retrying. The deliveries carry `X-Slack-Retry-Num: 1` and `2`. Return 200 first and process afterwards, and deduplicate on `event_id` so an already-handled event is a no-op.

Do slash commands use the same signature scheme?+

Yes — same `v0=` construction and same signing secret, but the body is form-encoded rather than JSON. Interactive components and shortcuts add a `payload` form field containing JSON, which trips up verifiers that assume the body is always JSON.

Other providers

Testing something else?

One click

Point Slack at a URL and watch the payload land.

api.slack.com/apps → your app → Event Subscriptions → Request URL — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second