How to Test Slack Webhooks with HookCap
By HookCap Team
How to Test Slack Webhooks with HookCap
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.
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 (or your local handler URL if doing verification first)
- 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 |
After saving, Slack starts delivering events to your endpoint.
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. You can use a different HookCap endpoint to separate interactivity payloads from event subscriptions, or use the same one and differentiate by payload structure.
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
}
The key fields to note:
type— alwaysevent_callbackfor real events (vs.url_verificationfor the challenge)event.type— the specific event (message,reaction_added,app_mention, etc.)event_id— unique per delivery, use for deduplicationteam_id— the workspace that generated the event
Interactivity payload
Interactivity payloads are different. They arrive as a form-encoded POST with a single payload field containing JSON:
Content-Type: application/x-www-form-urlencoded
payload=%7B%22type%22%3A%22block_actions%22%2C...%7D
When decoded, the payload looks like:
{
"type": "block_actions",
"user": {
"id": "U2147483697",
"name": "jsmith"
},
"trigger_id": "13345224609.8534564800.6f8ab1f0f3c9060c0c24a0ef07",
"channel": {
"id": "C2147483705",
"name": "general"
},
"actions": [
{
"type": "button",
"action_id": "approve_request",
"block_id": "approval_block",
"text": {
"type": "plain_text",
"text": "Approve"
},
"value": "request_123"
}
]
}
HookCap captures the raw request body, so you can see both the URL-encoded form and the decoded JSON payload. This is useful for debugging encoding issues.
Slash command payload
Slash commands also arrive as form-encoded POST data:
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...
&trigger_id=13345224609.738474920.8088930838d88f008e0
The response_url is important — it’s a one-time URL you can POST back to within 30 minutes to send a delayed response. HookCap captures this URL in the payload, making it easy to find when debugging response timing issues.
Auto-Forward to Your Local Dev Server
With HookCap’s auto-forward feature (Pro plan), Slack events are simultaneously captured in HookCap and forwarded to your local development server. This gives you both a persistent event log and live local processing.
Set up auto-forward to point to your local server:
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
- You can inspect the event in HookCap at any time
This eliminates the need for a separate tunnel tool. Your HookCap endpoint is the stable public URL Slack talks to, and auto-forward handles getting events to localhost.
Replaying Slack Events
Replay is where HookCap saves the most time during Slack app development. Instead of re-triggering events in Slack (posting messages, clicking buttons, typing slash commands), you replay captured events directly to your local server.
Common replay scenarios:
- You changed how your bot responds to
app_mentionevents and want to re-test with the same message payload - A button click handler failed on a specific
block_actionspayload and you need to reproduce it - You want to test that your slash command handler is idempotent by replaying the same command twice
- You need to test error handling — replay an event while your database is intentionally down
Replay preserves headers. Slack’s original headers (X-Slack-Signature, X-Slack-Request-Timestamp) are included in the replay. If your handler verifies signatures, the replayed signature will still match the original payload.
Slack Request Verification
Slack signs every outgoing request using your app’s Signing Secret. Your handler should verify this signature to ensure the request actually came from Slack.
How Slack signing works
Slack computes an HMAC-SHA256 signature over:
v0:{timestamp}:{request_body}
The signature is sent in the X-Slack-Signature header, and the timestamp in X-Slack-Request-Timestamp.
Verification in Node.js
const crypto = require('crypto');
function verifySlackRequest(req, signingSecret) {
const timestamp = req.headers['x-slack-request-timestamp'];
const signature = req.headers['x-slack-signature'];
// Reject requests older than 5 minutes to prevent replay attacks
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)
);
}
The raw body requirement
Like Stripe and Shopify, Slack’s signature is computed over the raw request body as bytes. If your framework parses the body before your verification middleware runs, the signature check fails.
In Express:
// Webhook routes use raw body
app.post('/slack/events', express.raw({ type: 'application/json' }), slackEventsHandler);
app.post('/slack/interactions', express.urlencoded({ extended: true }), slackInteractionsHandler);
// JSON parser for everything else
app.use(express.json());
Note that interactivity payloads use application/x-www-form-urlencoded, not application/json. You need the URL-encoded parser for those routes.
Verification during replay
When replaying events from HookCap, the original X-Slack-Signature and X-Slack-Request-Timestamp headers are included. The signature still matches the payload, but the timestamp will be old — your 5-minute staleness check will reject it.
For development, skip the timestamp check 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 response timeout
Slack expects your endpoint to respond within 3 seconds. If your handler takes longer, Slack considers it a failure and may retry. For long-running operations, respond with 200 OK immediately and process asynchronously:
app.post('/slack/events', (req, res) => {
// Acknowledge immediately
res.status(200).send();
// Process in background
processEvent(req.body).catch(console.error);
});
Duplicate event delivery
Slack may send the same event multiple times, especially if your endpoint was slow to respond. Use the event_id field for deduplication:
const processedEvents = new Set();
app.post('/slack/events', (req, res) => {
const { event_id } = req.body;
if (processedEvents.has(event_id)) {
return res.status(200).send();
}
processedEvents.add(event_id);
// In production, use Redis or a database instead of an in-memory Set
handleEvent(req.body);
res.status(200).send();
});
Bot messages triggering bot events
If your bot posts a message in a channel it is subscribed to message.channels events for, Slack sends your bot an event about its own message. This creates an infinite loop if your handler responds to every message. Filter out bot messages:
if (event.bot_id || event.subtype === 'bot_message') {
return res.status(200).send();
}
Interactivity response_url expiry
The response_url in interactivity and slash command payloads expires after 30 minutes. If you capture a payload in HookCap and replay it later, the response_url will no longer work. This doesn’t affect testing your handler’s internal logic, but any attempt to POST back to the response_url during replay will fail.
Token vs. Signing Secret
Older Slack apps used the token field in the payload for verification. This is deprecated. Always use the X-Slack-Signature header with your Signing Secret instead. The token field is still sent in payloads for backwards compatibility, but you should not rely on it.
Full Slack Webhook Development Workflow
- Create a HookCap endpoint and configure it in your Slack app settings (Event Subscriptions and/or Interactivity)
- Handle URL verification using auto-forward to your local server or a temporary tunnel
- Trigger events by posting messages, clicking buttons, or running slash commands in your Slack workspace
- Inspect payloads in HookCap — understand the exact structure before writing or modifying your handler
- Write your handlers against actual payload shapes, distinguishing between event callbacks, interactivity payloads, and slash commands
- Replay events to your local server as you iterate, using auto-forward for live testing
- Test edge cases by replaying specific payloads: messages with attachments, button clicks from different users, slash commands with unusual arguments
- Verify signature handling works correctly before deploying to production
HookCap’s persistent event history means you capture real Slack payloads once and replay them throughout your development cycle — no need to keep typing messages or clicking buttons in Slack to re-trigger events.
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 and longer history retention.