Idempotency
Duplicate deliveries are routine, not an edge case. Suppress them with a store.
Providers retry on non-2xx, and several deliver at-least-once even when you succeed. Duplicate deliveries are routine, not an edge case — any handler that charges a card or sends an email needs to be safe against seeing the same event twice.
import { createWebhookHandler, memoryIdempotencyStore } from 'webhooks-sdk'
const handler = createWebhookHandler({
provider: stripe({ secret }),
idempotency: memoryIdempotencyStore(),
on: { /* … */ },
})
With a store configured, a redelivered event is suppressed before dispatch
and the result reports outcome: 'duplicate'.
How suppression works
The store is keyed on `${provider.id}:${event.id}`, and an event is
only remembered after your handlers succeed — so a delivery that failed
mid-handler is re-run on retry instead of being acknowledged as a duplicate.
An event whose provider supplies no usable id skips deduplication entirely
rather than collapsing every delivery onto one key.
The in-memory store takes tuning options:
memoryIdempotencyStore({
ttlMs: 24 * 60 * 60 * 1000, // default: 24 hours
maxSize: 10_000, // default: oldest evicted first
})
A day’s TTL is plenty — retry windows are hours, not weeks.
The in-memory store is single-process
memoryIdempotencyStore() suits one long-lived process. On serverless or
across several instances, each instance keeps its own map and catches
nothing. There, implement the two-method IdempotencyStore interface over
shared storage — Redis, a KV namespace, a Durable Object:
import type { IdempotencyStore } from 'webhooks-sdk'
const redisStore: IdempotencyStore = {
async seen(key) {
return (await redis.exists(`webhook:${key}`)) === 1
},
async remember(key, ttlMs) {
await redis.set(`webhook:${key}`, '1', { PX: ttlMs ?? 24 * 60 * 60 * 1000 })
},
}
Replay protection for body-only HMACs
Some schemes (GitHub’s, for one) sign the body alone: the signature proves who sent the request but not when, so the same bytes replayed a week later verify perfectly. For those providers the store doubles as replay protection, keyed on a digest of the signed body — not on the delivery id, which lives in an unsigned header a replay could mint fresh.
See scheme families for which providers need this.