How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Slack webhook?
With the Events API, Slack sends an HTTP POST to your app's Request URL when something your app subscribed to happens in a workspace — a message is posted, a reaction is added, your app is mentioned. The body is a wrapper of type event_callback; the thing that happened is in event, and event_id identifies the delivery.
Not to be confused with incoming webhooks, which run the other way: a URL under hooks.slack.com that you post a message to. Same word, opposite direction.
What a Slack webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Slack-Signature: v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503
X-Slack-Request-Timestamp: 1767225600
X-Slack-Retry-Num: 1
X-Slack-Retry-Reason: http_timeout
Content-Type: application/json
{
"token": "DEPRECATED-verification-token",
"team_id": "T0EXAMPLE1",
"api_app_id": "A0EXAMPLE2",
"event": {
"type": "app_mention",
"user": "U0EXAMPLE3",
"text": "<@U0BOTEXAMPLE> deploy status?",
"ts": "1767225600.000200",
"channel": "C0EXAMPLE4",
"event_ts": "1767225600.000200"
},
"type": "event_callback",
"event_id": "Ev0EXAMPLE5",
"event_time": 1767225600,
"authorizations": [
{
"team_id": "T0EXAMPLE1",
"user_id": "U0BOTEXAMPLE",
"is_bot": true
}
]
}
The top-level token is the old verification token — a static string that is deprecated in favour of signed requests; do not authenticate with it. Timestamps such as ts are strings, and a message's ts doubles as its id within the channel.
Headers Slack sends
| Header | Example | What it is |
|---|---|---|
X-Slack-Signature | v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d2… | HMAC-SHA256, hex, with a v0= version prefix. |
X-Slack-Request-Timestamp | 1767225600 | Unix time the request was sent. It is part of the signed string. |
X-Slack-Retry-Num | 1 | Present only on a retry: which attempt this is. |
X-Slack-Retry-Reason | http_timeout | Present only on a retry: why the previous attempt failed. |
Content-Type | application/json | Events are JSON. (Slash commands and interactivity are form-encoded instead.) |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How Slack signs a request
Slack concatenates a version, the timestamp header and the raw body into v0:{timestamp}:{body}, computes an HMAC-SHA256 with your app's signing secret, and sends v0= plus the hex digest in X-Slack-Signature.
Verify it by rebuilding that string, and reject any request whose timestamp is more than about five minutes from your clock — the timestamp is signed precisely so that a captured request cannot be replayed later. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// rawBody: string of the body exactly as received
function verifySlack(rawBody, signature, timestamp, signingSecret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v0=' + crypto.createHmac('sha256', signingSecret)
.update('v0:' + timestamp + ':' + rawBody).digest('hex');
const a = Buffer.from(signature || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The handshake, the three-second rule and retries
Before Slack will save a Request URL it sends a url_verification request containing a challenge string, and the endpoint must answer with that same value. The webhook URLs created on this site do this automatically — a JSON body with "type": "url_verification" gets its challenge echoed back — so the URL can be saved in Slack's settings and real events inspected.
After that, each event must be acknowledged with a 2xx within three seconds. If it is not, Slack retries up to three times with increasing delays, per its documentation at the time of writing, marking each retry with X-Slack-Retry-Num and X-Slack-Retry-Reason. An app that fails most of its deliveries over a sustained period can have its event subscriptions disabled.
Catch a real Slack event
- Press Create webhook URL at the top of this page and copy the address.
- At api.slack.com/apps, open your app (or create one in a test workspace) and go to Event Subscriptions.
- Turn events on and paste the address as the Request URL. Slack sends the challenge and shows Verified.
- Subscribe to a bot event such as
app_mention, save, and reinstall the app if prompted. - Mention the app in a channel it has joined and read the event in the inbox above.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Slow handlers cause duplicate work. A reply after three seconds still gets a retry. Acknowledge first; if you cannot, de-duplicate on
event_id. - Your bot hears itself. Subscribing to message events delivers your own bot's messages too. Ignore events carrying a
bot_idor you will build a loop. - Three surfaces, three formats. Events are JSON; slash commands are form fields; interactive components are a form field named
payloadcontaining JSON. All three use the same signature scheme.
api-tester.com is independent and is not affiliated with Slack. Providers change header names, limits and retry schedules; treat the official Slack documentation as the source of truth.
Related examples and tools
Discord Webhook Example
A Discord webhook runs the other way from most: you send the request, and Discord posts it in a channel.
Twilio Webhook Example
Twilio's webhooks are form posts, not JSON, and the signature covers your URL as well as the parameters.
GitLab Webhook Example
The JSON a GitLab project webhook posts on a push, the headers that describe it, and the secret token that is compared rather than computed.
Telegram Webhook Example
The Update object Telegram posts to a bot's webhook, the optional secret-token header, and the trick of answering with an API call in the response body.
SendGrid Webhook Example
SendGrid's Event Webhook posts a batch — a JSON array of email events — and signs it with ECDSA rather than an HMAC.
Slack API Tester
Call Slack Web API methods such as auth.test and chat.postMessage from the browser.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks with a timestamp in the signature
References
- Official Slack documentation — The provider's own guide to the requests described on this page.
- Slack Developer Docs: verifying requests from Slack — The signing-secret check, step by step.
- Slack Developer Docs: using HTTP Request URLs — The URL verification challenge and how to respond to events.
- Slack Developer Docs: events reference — Every event type and its payload.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Slack webhook FAQ
What does a Slack Events API payload look like?
A JSON object of type event_callback with team_id, api_app_id, event_id, event_time, authorizations and an event object describing what happened, for example an app_mention or a message.
How do I verify a request came from Slack?
Build the string "v0:timestamp:rawbody" from the X-Slack-Request-Timestamp header and the raw body, compute an HMAC-SHA256 with the app's signing secret, prefix the hex digest with v0= and compare it to X-Slack-Signature. Reject timestamps older than about five minutes.
What is the Slack url_verification challenge?
A one-time request Slack sends when you set a Request URL. Your endpoint must respond with the challenge value from the body to prove it controls the URL.
How long do I have to respond to a Slack event?
Three seconds. Return a 2xx immediately and do the real work asynchronously.
Does Slack retry failed events?
Yes, up to three times according to Slack's documentation at the time of writing. Retries carry X-Slack-Retry-Num and X-Slack-Retry-Reason headers.