1. Return 200 before you do the work

The number one reason endpoints get auto-paused is slow handlers. Treat webhooks like a mailbox - accept the letter, drop it in a queue, get out.
app.post("/webhooks", express.raw(...), (req, res) => {
  const event = verify(req.body, req.headers["geldstuck-signature"], secret);
  await queue.push(event);   // enqueue, don't process
  res.sendStatus(200);        // return immediately
});
Downstream workers do the actual work on their own schedule.

2. Make handlers idempotent

Webhooks will occasionally retry. Your code will occasionally see duplicate events. Dedupe on event.id:
const firstTime = await redis.set(
  `gs:event:${event.id}`,
  "1",
  { NX: true, EX: 7 * 24 * 3600 },
);
if (firstTime !== "OK") return;

3. Verify. Every. Time.

Never skip verification - not in “local dev,” not for a “quick prototype.” An attacker who guesses your endpoint URL can forge events. Drop in the reference verifier and forget it.

4. One endpoint per environment

Production, staging, local. Each has its own URL, its own secret, its own delivery log. Sharing endpoints across environments turns debugging into archaeology.

5. Subscribe narrowly, log everything

Subscribe specific handlers to the exact event types they care about. Then subscribe a second “firehose” endpoint to * that writes to your audit log - so you always have a record, even for events you don’t yet have code for.

6. Reject stale timestamps

The signature-verification helpers reject timestamps older than 5 minutes by default. Keep the default - it’s your only defense against replay attacks.

7. Don’t trust the X-Geldstuck-Event header

It’s convenient for routing, but anyone can set an HTTP header. Always verify the signature first, then trust event.type from the decoded body.

8. Respond with the right status code

  • 200 – processed.
  • 400 – signature failed, malformed payload. We won’t retry.
  • 5xx / timeout – we’ll retry with backoff. Use this if your downstream queue is unavailable.
Returning 400 tells us to give up. Only use it for problems that won’t resolve by retrying - like a bad signature. Don’t return 400 for a transient database outage; return 503 so we retry.

9. Monitor delivery health

  • Alert on webhook.delivery.failed meta-events.
  • Watch p95 handler latency - if it climbs past 5 seconds you’re close to the 10-second cutoff.
  • Watch your dead-letter count. A non-zero number after an incident tells you exactly what to replay.

10. Handle unknown event types gracefully

We add new event types in minor version bumps. Your handler should:
switch (event.type) {
  case "kyc.completed": /* ... */ break;
  case "transaction.status.changed": /* ... */ break;
  default:
    logger.info({ type: event.type }, "unhandled webhook event");
}
Log and move on - don’t throw. A new event type should never break your integration.