Deduplication and replays
Providers deliver at least once: a timeout or a 5xx makes them retry, so the same event can arrive several times. A signature check alone accepts every copy. A dedupe store remembers the events you’ve seen and rejects repeats with DUPLICATE_EVENT.
import { MemoryDedupeStore, verifyWebhook, WebhookErrorCode } from 'verihook';
const dedupeStore = new MemoryDedupeStore({ ttlMs: 300_000, maxSize: 10_000 });
const result = await verifyWebhook('stripe', request, secret, { dedupeStore });
if (!result.valid && result.code === WebhookErrorCode.DUPLICATE_EVENT) { return Response.json({ received: true, duplicate: true }); // 2xx stops retries}
try { await processEvent(result.event);} catch (err) { await dedupeStore.delete(result.dedupeKey!); // let the retry through throw err;}The adapters do both for you: duplicates get 200, and if your handler throws or responds with a 5xx the key is released so the provider’s retry is processed.
What the key is
Section titled “What the key is”The key only uses data covered by the provider’s signature, so an attacker can’t change it to sneak a replay past the store:
- a signed ID header:
svix-id(Svix, Resend, Clerk),webhook-idoridempotency-key(GitLab),paypal-transmission-id,twitch-eventsub-message-id; - Mailgun’s signed
token; - an ID in the signed body:
id,event_id,msg_id,notificationId, or a WhatsApp message ID; - otherwise a SHA-256 of the body.
Override it with options.eventId if you know better.
Shared stores
Section titled “Shared stores”MemoryDedupeStore lives in one process. Serverless functions and multiple replicas each have their own memory, so use a shared store. Implement the DedupeStore interface:
import type { DedupeStore } from 'verihook';
const store: DedupeStore = { // Returns true if the key was already seen; otherwise records it for ttlMs and returns false. async hasOrSet(key, ttlMs) { /* ... */ }, // Optional: lets the adapters release a key when your handler fails. async delete(key) { /* ... */ },};Ready-made examples are in the repository: Upstash Redis for edge runtimes and Cloudflare Workers KV.