Slack's platform sends webhooks for everything: messages posted in channels, button clicks in interactive messages, slash command invocations, workflow step callbacks, and app lifecycle events. If you're building a Slack app, you need a reliable way to capture and inspect these events during development.
The challenge is familiar: Slack requires a publicly reachable HTTPS URL, but your app is running on localhost:3000. You need to see the exact payload structure Slack sends, replay events to test edge cases, and iterate quickly without redeploying.
This guide covers using HookCap as your Slack webhook testing tool.
Originally published at hookcap.dev
Types of Slack Webhooks
Slack sends events through several different mechanisms, each with its own payload format:
| Type | Where to configure | What it sends |
|---|---|---|
| Event Subscriptions | App Settings > Event Subscriptions | Channel messages, reactions, member joins, file uploads |
| Interactivity | App Settings > Interactivity & Shortcuts | Button clicks, modal submissions, menu selections |
| Slash Commands | App Settings > Slash Commands | User-typed commands like /deploy or /ticket
|
| Incoming Webhooks | App Settings > Incoming Webhooks | Outbound only (you send TO Slack) — not relevant here |
Event Subscriptions and Interactivity are the two you will work with most. They have different payload structures, different verification requirements, and different response expectations.
Setting Up HookCap for Slack Webhooks
1. Create a HookCap endpoint
Sign in at hookcap.dev and create a new endpoint. You get a permanent HTTPS URL like:
https://hook.hookcap.dev/ep_a1b2c3d4e5f6
This URL stays active across sessions. Register it once in your Slack app and it keeps receiving events.
2. Handle the URL verification challenge
When you first enter your HookCap endpoint URL in Slack's Event Subscriptions settings, Slack sends a verification challenge — a POST request with this body:
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
"type": "url_verification"
}
Slack expects your endpoint to respond with the challenge value in the response body. HookCap captures this delivery for inspection, but since it responds with a 200 OK and not the challenge string, the verification will not pass directly.
To pass URL verification:
Use HookCap's auto-forward feature (Pro plan) to forward events to your local dev server, which handles the challenge response. Your local handler returns the challenge value, and Slack completes verification. After verification succeeds, all subsequent events continue to flow through HookCap for inspection.
Alternatively, temporarily point Slack to your local handler via a tunnel for the initial verification, then switch the URL to your HookCap endpoint for ongoing development.
3. Configure Event Subscriptions
In your Slack app settings under Event Subscriptions:
- Toggle Enable Events on
- Enter your HookCap endpoint URL
- Subscribe to the bot events you need
Common bot events to subscribe to:
| Event | When it fires |
|---|---|
message.channels |
A message is posted to a public channel your bot is in |
message.groups |
A message is posted to a private channel your bot is in |
message.im |
A direct message is sent to your bot |
app_mention |
Your bot is @mentioned in a channel |
reaction_added |
A reaction emoji is added to a message |
member_joined_channel |
A user joins a channel |
channel_created |
A new channel is created |
4. Configure Interactivity (if needed)
If your app uses buttons, menus, modals, or shortcuts, go to Interactivity & Shortcuts and enter your HookCap endpoint URL as the Request URL.
Inspecting Slack Event Payloads
Event Subscriptions payload
Every event delivery from Slack follows this envelope structure:
{
"token": "ZZZZZZWSxiZZZ2yIvs3peJ",
"team_id": "T061EG9R6",
"api_app_id": "A0MDYCDME",
"event": {
"type": "message",
"channel": "C2147483705",
"user": "U2147483697",
"text": "Hello world",
"ts": "1355517523.000005",
"event_ts": "1355517523.000005",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev0PV52K25",
"event_time": 1355517523
}
Interactivity payload
Interactivity payloads arrive as form-encoded POST with a single payload field containing JSON:
{
"type": "block_actions",
"user": { "id": "U2147483697", "name": "jsmith" },
"trigger_id": "13345224609.8534564800.6f8ab1f0f3c9060c0c24a0ef07",
"actions": [{
"type": "button",
"action_id": "approve_request",
"value": "request_123"
}]
}
Slash command payload
token=gIkuvaNzQIHg97ATvDxqgjtO
&team_id=T0001
&channel_id=C2147483705
&user_id=U2147483697
&command=%2Fdeploy
&text=production+v1.2.3
&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2F...
Auto-Forward to Your Local Dev Server
With HookCap's auto-forward feature (Pro plan), Slack events are simultaneously captured and forwarded to your local development server.
Set up auto-forward:
Forward URL: http://localhost:3000/slack/events
Now when Slack sends an event:
- HookCap captures and stores the full request
- HookCap forwards the request to your local server
- Your local server processes the event and returns a response
This eliminates the need for a separate tunnel tool.
Slack Request Verification
Slack signs every outgoing request using your app's Signing Secret:
const crypto = require("crypto");
function verifySlackRequest(req, signingSecret) {
const timestamp = req.headers["x-slack-request-timestamp"];
const signature = req.headers["x-slack-signature"];
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5;
if (parseInt(timestamp) < fiveMinutesAgo) return false;
const sigBasestring = `v0:${timestamp}:${req.rawBody}`;
const computed = "v0=" + crypto
.createHmac("sha256", signingSecret)
.update(sigBasestring, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signature)
);
}
Important: During replay, the timestamp check will fail because the original timestamp is old. For development, skip it conditionally:
if (process.env.NODE_ENV !== "development") {
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5;
if (parseInt(timestamp) < fiveMinutesAgo) return false;
}
Common Slack Webhook Gotchas
3-second timeout: Slack expects a response within 3 seconds. For long operations, respond with 200 OK immediately and process async.
Duplicate delivery: Use event_id for deduplication — Slack may send the same event multiple times.
Bot message loop: Filter out bot messages to avoid infinite loops when your bot posts in subscribed channels:
if (event.bot_id || event.subtype === "bot_message") {
return res.status(200).send();
}
response_url expiry: Interactivity and slash command response_url values expire after 30 minutes — replaying old events won't be able to POST back to Slack.
Full Development Workflow
- Create a HookCap endpoint and configure it in your Slack app settings
- Handle URL verification using auto-forward or a temporary tunnel
- Trigger events in your Slack workspace
- Inspect payloads in HookCap
- Write handlers against actual payload shapes
- Replay events as you iterate
- Test edge cases with specific replayed payloads
- Verify signature handling before deploying to production
Free tier available at hookcap.dev — includes capture, inspect, replay, and real-time streaming. Pro plans start at $12/month for auto-forward to localhost.
Top comments (0)