Providers / April 24, 2026

Slack webhooks: how to receive events reliably (Events API + Incoming Webhooks)

Slack Events API delivery flow with signature verification and retries

When people say "Slack webhook," they could mean two completely different things:

  1. Incoming Webhooks — your application POSTs to a Slack URL to send a message into a Slack channel. Outbound from you.
  2. Events API — Slack POSTs to your URL when events happen in workspaces your app is installed in. Inbound to you.

Both involve HTTP and JSON. Both are called "webhooks" colloquially. They're solving opposite problems.

This guide covers both, with focus on the inbound side — receiving Slack events reliably — since that's where production teams need infrastructure.

Slack Incoming Webhooks (outbound)

A Slack Incoming Webhook is a URL like:

https://hooks.slack.com/services/T0123/B0456/xxxx

POST a JSON body to send a message:

{ "text": "Deploy succeeded", "channel": "#ops" }

Use cases: alerts from monitoring tools, deploy notifications, simple bot output. The whole pattern is one POST per message — no signing, no retries, no verification beyond the URL itself being a secret.

This is fine for one-off integrations. It's not fine when you're forwarding events from many providers into Slack and want a single reliable pipeline, when the URL gets leaked and you need rotation, or when you need delivery confirmation, retries on failure, and a delivery history.

For those cases, put a relay in front: events flow into Hooksbase, get transformed and routed, and dispatch to the Slack Incoming Webhook URL as one of Hooksbase's HTTP destinations. You get retries, delivery history, and easy URL rotation without touching the producers.

Slack Events API (inbound)

The Events API is what Slack uses to notify your app when something happens — a message in a channel your bot sees, a user joining, an app mention, a reaction added.

A message event:

{
  "token": "verification_token",
  "team_id": "T0123",
  "api_app_id": "A0456",
  "event": {
    "type": "message",
    "channel": "C0789",
    "user": "U2468",
    "text": "Hey bot",
    "ts": "1714000000.000200"
  },
  "type": "event_callback",
  "event_id": "Ev0123",
  "event_time": 1714000000
}

Headers:

X-Slack-Signature: v0=abc...
X-Slack-Request-Timestamp: 1714000000
Content-Type: application/json

Slack URL verification

When you first register an Events API URL, Slack POSTs a url_verification challenge:

{ "type": "url_verification", "challenge": "abc123..." }

Your endpoint must respond with the challenge value. If it doesn't, Slack refuses to register the URL.

Slack's signature scheme

Every event POST is signed with HMAC-SHA256:

signature = "v0=" + hex(hmac_sha256(signing_secret, "v0:" + timestamp + ":" + raw_body))

Verification:

  1. Read the raw body before any JSON parsing
  2. Reject anything where now - timestamp > 300 seconds (replay protection)
  3. Recompute the signature and compare constant-time with X-Slack-Signature

Slack's retry policy

Slack expects a 2xx response within 3 seconds. If you don't respond in time, or you respond with a non-2xx, Slack retries up to 3 times: nearly immediately, then after about 1 minute, then after about 5 minutes.

Three seconds is tight. If your handler does any synchronous work (database lookup, AI call, HTTP request to another service), you'll exceed the budget under load. The fix is universal: acknowledge fast, work async.

DIY: minimal Slack Events handler in Node

import { createHmac, timingSafeEqual } from 'crypto'

const signingSecret = process.env.SLACK_SIGNING_SECRET!

export async function POST(req: Request) {
  const sig = req.headers.get('x-slack-signature')!
  const ts = req.headers.get('x-slack-request-timestamp')!
  const body = await req.text()

  // Replay protection
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return new Response('Stale', { status: 400 })
  }

  // Signature check
  const expected =
    'v0=' +
    createHmac('sha256', signingSecret).update(`v0:${ts}:${body}`).digest('hex')
  if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response('Bad signature', { status: 400 })
  }

  const payload = JSON.parse(body)

  // URL verification handshake (one-time when setting up the URL)
  if (payload.type === 'url_verification') {
    return new Response(payload.challenge)
  }

  // Acknowledge fast, process async
  queueWork(payload)

  return new Response('ok', { status: 200 })
}

Production needs more:

  • A queue for async work (3-second budget is the hard rule)
  • Idempotency on event_id (Slack will retry if you're slow)
  • Replay path for events older than Slack's retry window
  • Filtering — most apps see floods of low-value events
  • Signing-secret rotation
  • Delivery history when something silently stops working

Hooksbase: receive Slack events without rebuilding the rest

Hooksbase treats Slack as one of five pre-verified providers. After the request passes Hooksbase ingest auth, the signature is verified at the relay edge before the payload reaches your endpoint.

Setup:

  1. Create a webhook in Hooksbase with provider: slack
  2. Paste your Slack signing secret (encrypted at rest)
  3. Put a small forwarder at the Slack Events API request URL. It should preserve the raw body and Slack signature headers, then POST to the Hooksbase ingest URL with Authorization: Bearer <ingest secret>. For Slack URL verification, pass the request through and return Hooksbase's response.
  4. Add your application URL as the destination

You get:

  • Signature verification before the event reaches your code
  • Idempotency for your destination — every dispatch includes a unique webhook-id header you can dedupe on across retries. Slack's event_id is captured as provider.sourceId and queryable on every delivery.
  • Retries with exponential backoff to your endpoint after Hooksbase accepts the event, beyond Slack's short retry sequence
  • Acknowledge quickly — your forwarder can hand the verified request to Hooksbase and return 2xx to Slack without waiting on your app's slower downstream work
  • Routing rules by event type — send message events to one destination, app_mention to another, reaction_added to a third
  • Provider-aware queryable fieldsprovider.eventType, provider.verified on every delivery
  • Deterministic replay — re-run an event with the same payload bytes after a fix while the payload is retained
  • Delivery history and DLQ

Common Slack event use cases for AI agents

  • Mention-triggered agentapp_mention events trigger an agent that interprets the user's request, calls tools, and replies in-thread
  • Channel monitoring agentmessage.channels events from a specific channel trigger an agent that summarizes or labels traffic
  • Onboarding agentteam_join events trigger an agent that DMs new users with guided setup
  • Reaction-driven workflowreaction_added with a specific emoji triggers an agent that takes an action on the referenced message

Where to go next

Start free at app.hooksbase.com, then use Starter+ provider verification when you're ready to receive signed Slack events through Hooksbase.

Related guides