Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Slack By James Joyner IV · · 12 min read

Hardening the Slack Events API HTTP Endpoint: URL Verification, Retries, and Dedup

Run a public Slack Events API endpoint safely: url_verification, the 3-second ack, retry deduplication, and signatures. AI drafts it; you review the edges.

  • #slack
  • #chatops
  • #events-api
  • #reliability

When I first stood up a public Events API endpoint for a Slack bot, it passed Slack’s setup check, processed events, and looked done. Then a downstream API got slow one afternoon and my bot started double-posting — sometimes triple-posting — the same alert. The cause wasn’t a bug in my logic. It was that I’d ignored two facts about how Slack delivers events: it expects an acknowledgment within three seconds, and if it doesn’t get one, it retries the same event. My slow handler missed the window, Slack retried, and I processed the same event three times. The endpoint “worked” right up until production load made it visibly wrong.

This is the kind of code I now write with AI, and it’s a textbook case for treating AI as a fast junior engineer. The model produces a handler that responds to events and passes the setup challenge — and it routinely omits dedup, signature verification, or the fast-ack pattern, because the happy path looks complete. A human reviews the failure edges before this faces the public internet. Slack’s retry behavior is unforgiving of a half-finished handler.

Step zero: URL verification

Before Slack sends events, it sends a one-time url_verification challenge. You must echo the challenge value back. Get this wrong and you can’t even save the endpoint:

app.post('/slack/events', (req, res) => {
  if (req.body.type === 'url_verification') {
    return res.status(200).send(req.body.challenge);
  }
  // ...handle real events
});

If you use Bolt with the built-in receiver, this is handled for you. If you roll your own HTTP layer, this is step one.

Verify the signature before you trust anything

Every request carries X-Slack-Signature and X-Slack-Request-Timestamp. Verify the HMAC and reject stale timestamps before parsing the body as meaningful. This endpoint is on the public internet; the signature is the only thing standing between you and a forged event:

const crypto = require('crypto');

function verify(req, signingSecret) {
  const ts = req.headers['x-slack-request-timestamp'];
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;   // replay window
  const base = `v0:${ts}:${req.rawBody}`;
  const mine = 'v0=' + crypto.createHmac('sha256', signingSecret)
    .update(base).digest('hex');
  const theirs = req.headers['x-slack-signature'];
  return crypto.timingSafeEqual(Buffer.from(mine), Buffer.from(theirs));
}

Use timingSafeEqual, not ===, and capture the raw body — re-serialized JSON won’t match the signature. This is non-negotiable security code; never let the model’s draft ship without you confirming the constant-time compare and replay window are intact.

Ack in under three seconds, then do the work

Slack wants a 200 within three seconds. Slow work after acking is fine; slow work before acking triggers retries. Ack first, process async:

app.post('/slack/events', (req, res) => {
  if (!verify(req, SIGNING_SECRET)) return res.sendStatus(401);
  if (req.body.type === 'url_verification') return res.send(req.body.challenge);

  res.sendStatus(200);                 // ack immediately
  queue.add(req.body);                 // process out of band
});

The moment you res.sendStatus(200), Slack is satisfied and won’t retry. Everything heavy — AI calls, database writes, posting messages — happens after, off the request path.

Deduplicate retries

Even with a fast ack, retries happen (network blips, brief outages). Slack marks retries with the X-Slack-Retry-Num header and, more reliably, every event has a stable event_id. Dedup on event_id:

async function process(envelope) {
  const id = envelope.event_id;
  if (await seen(id)) return;          // idempotency guard
  await markSeen(id, { ttl: 3600 });
  await handle(envelope.event);
}

A short-TTL store (Redis, even an in-memory LRU for a single instance) is enough. The event_id is stable across retries of the same event, which is exactly what you need for exactly-once processing.

Pro Tip: Don’t dedup on X-Slack-Retry-Num alone — it tells you a retry happened but the first delivery might never have reached you. event_id is the durable key; the retry header is just a useful signal for logging “Slack had to retry this.”

Always return 200 for events you ignore

If your endpoint returns a non-2xx for an event you simply don’t care about, Slack treats it as a failure and retries — and after enough failures, it may disable your event subscription entirely. Acknowledge everything, then decide internally whether to act:

res.sendStatus(200);
if (!shouldHandle(req.body.event)) return;   // ack, then ignore quietly

Disabled subscriptions are a miserable outage to debug because your bot just goes silent. Ack-then-ignore is the safe pattern.

Where AI helps and where you own it

The model writes the routing, the queue wiring, and the dedup store quickly, and I happily scaffold those with Cursor or Claude, iterating on the prompt in the prompt workspace. If you’d rather skip the public endpoint entirely, Socket Mode is a real alternative — but when you do need HTTP, these edges are mandatory. What stays human: the signature verification (review it line by line), the ack-first ordering, and the dedup key choice. Don’t hand the model your signing secret to “test” — give it fixtures and review the diff before anything faces the internet.

Conclusion

A public Events API endpoint looks done the moment it passes Slack’s setup check, and it isn’t. Echo the url_verification challenge, verify signatures with constant-time compare and a replay window, ack within three seconds and process async, deduplicate on event_id, and return 200 even for events you ignore. Let AI draft the structure fast while a human reviews every failure edge. More in the Slack category, and reusable prompts to scaffold a hardened receiver.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.