Slack Webhook: Example Payload, Headers and Signature

The JSON Slack's Events API posts to your Request URL, the one-time url_verification handshake, and the v0 signature that covers a timestamp as well as the body.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by Slack, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

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

HeaderExampleWhat it is
X-Slack-Signaturev0=a2114d57b48eac39b9ad189dd8316235a7b4a8d2…HMAC-SHA256, hex, with a v0= version prefix.
X-Slack-Request-Timestamp1767225600Unix time the request was sent. It is part of the signed string.
X-Slack-Retry-Num1Present only on a retry: which attempt this is.
X-Slack-Retry-Reasonhttp_timeoutPresent only on a retry: why the previous attempt failed.
Content-Typeapplication/jsonEvents 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

  1. Press Create webhook URL at the top of this page and copy the address.
  2. At api.slack.com/apps, open your app (or create one in a test workspace) and go to Event Subscriptions.
  3. Turn events on and paste the address as the Request URL. Slack sends the challenge and shows Verified.
  4. Subscribe to a bot event such as app_mention, save, and reinstall the app if prompted.
  5. 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_id or you will build a loop.
  • Three surfaces, three formats. Events are JSON; slash commands are form fields; interactive components are a form field named payload containing 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

Other webhooks with a timestamp in the signature

References

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.