Skip to content

Start typing to search the documentation.

Best practices

A small list of things that prevent the common production fires.

1. Make handlers idempotent

We deliver at-least-once. Network blips and our own retries produce duplicates. Your handler should treat duplicates as a non-event.

Dedup by event ID

The simplest pattern:

const eventId = `${event.verification_id}-${event.type}`;

const inserted = await db.query(
  `INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING 1`,
  [eventId],
);

if (inserted.rowCount === 0) {
  return;   // already processed
}

await applyVerification(event);

Use the verification_id from the event, it’s stable across retries.

2. Acknowledge quickly, work async

Return 2xx within a few seconds. If your handler does expensive work, database fan-out, downstream API calls, emails, push to a queue and return immediately.

app.post('/webhooks/self', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = SelfWebhooks.verify(req.body, req.headers, secret);
  await jobQueue.enqueue('process-verification', event);
  res.status(200).end();
});

Slow handlers get retried, which produces duplicates, which compounds. Fast ack + queue is the only stable pattern.

3. Treat each delivery in isolation

verification.completed is the only event delivered, so there’s no cross-event ordering to reason about. Don’t assume anything about the order of deliveries for different verifications either. If you need a verification’s current storage state, read it with sessions.get(...) rather than inferring it from the webhook.

4. Distinguish status carefully

verification.completed fires with four statuses. Don’t lump them:

StatusWhat it meansCommon handling
validProof verified, predicates passed.Mark user verified, unlock the gated feature.
invalidProof verified, a predicate failed.Tell the user why (e.g. age requirement); offer retry.
errorTechnical failure (bad proof, unsupported document).Tell the user to retry; alert ops if rate spikes.
expiredSession timed out.Generate a new session if the user is still around.

A common bug: treating any non-valid as a final rejection. invalid and error are recoverable; expired definitely is.

5. Verify before doing anything

Don’t read event.type until SelfWebhooks.verify(...) has returned. An attacker can send any body to your endpoint; only the signature proves it came from us.

// Wrong:
app.post('/webhooks/self', (req, res) => {
  const event = JSON.parse(req.body);     // ← trusting unverified body
  if (event.type === 'verification.completed') {
    grantAccess(event.external_uuid);     // ← attacker can call this
  }
});

// Right:
app.post('/webhooks/self', express.raw({ type: 'application/json' }), (req, res) => {
  const event = SelfWebhooks.verify(req.body, req.headers, secret);
  // ...now safe to act on event
});

6. Return the right status codes

Your responseWhat we do
2xxDelivery successful. No retry.
4xx (except 408, 429)Treated as a permanent rejection. No retry.
408, 429, 5xxRetried with backoff.
Timeout / connection refusedRetried with backoff.

So:

  • Return 400 on signature failure. You want it dropped, not retried.
  • Return 500 (or just throw) on transient errors. You want it retried.
  • Don’t return 400 because of a database hiccup. You’ll silently drop events.

7. Use one endpoint per environment

Don’t share a single endpoint between staging and production. They’ll have different secrets and different traffic profiles. Register one endpoint for test and one for live, each with its own URL and signing secret.

8. Monitor failures on your side

Alert on your own handler’s error rate (the 5xx and signature-failure responses it returns). A handler that’s been silently failing is a known production incident pattern, and you’ll spot it faster from your own metrics than from the deliveries. Failed deliveries are retried automatically, so once you fix the handler the backlog drains on its own.

9. Log the verification ID

Log event.verification_id on every handler invocation:

log.info({ msg: 'webhook_received', verificationId: event.verification_id, type: event.type });

When you ask support about a missing or wrong delivery, that’s the ID to quote.