· 9 min read

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:

TypeWhere to configureWhat it sends
Event SubscriptionsApp Settings > Event SubscriptionsChannel messages, reactions, member joins, file uploads
InteractivityApp Settings > Interactivity & ShortcutsButton clicks, modal submissions, menu selections
Slash CommandsApp Settings > Slash CommandsUser-typed commands like /deploy or /ticket
Incoming WebhooksApp Settings > Incoming WebhooksOutbound 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:

  1. Toggle Enable Events on
  2. Enter your HookCap endpoint URL (or your local handler URL if doing verification first)
  3. Subscribe to the bot events you need

Common bot events to subscribe to:

EventWhen it fires
message.channelsA message is posted to a public channel your bot is in
message.groupsA message is posted to a private channel your bot is in
message.imA direct message is sent to your bot
app_mentionYour bot is @mentioned in a channel
reaction_addedA reaction emoji is added to a message
member_joined_channelA user joins a channel
channel_createdA 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 — always event_callback for real events (vs. url_verification for the challenge)
  • event.type — the specific event (message, reaction_added, app_mention, etc.)
  • event_id — unique per delivery, use for deduplication
  • team_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:

  1. HookCap captures and stores the full request
  2. HookCap forwards the request to your local server
  3. Your local server processes the event and returns a response
  4. 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_mention events and want to re-test with the same message payload
  • A button click handler failed on a specific block_actions payload 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

  1. Create a HookCap endpoint and configure it in your Slack app settings (Event Subscriptions and/or Interactivity)
  2. Handle URL verification using auto-forward to your local server or a temporary tunnel
  3. Trigger events by posting messages, clicking buttons, or running slash commands in your Slack workspace
  4. Inspect payloads in HookCap — understand the exact structure before writing or modifying your handler
  5. Write your handlers against actual payload shapes, distinguishing between event callbacks, interactivity payloads, and slash commands
  6. Replay events to your local server as you iterate, using auto-forward for live testing
  7. Test edge cases by replaying specific payloads: messages with attachments, button clicks from different users, slash commands with unusual arguments
  8. 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.