@rovidev/webhooks-engine
v1.0.0
Published
Idempotent webhook processing: signature verification, deduplication, retries with backoff and dead-letter queue.
Maintainers
Readme
webhooks-engine
Idempotent webhook processing for Node.js. Handles the parts that usually cause bugs with providers like Stripe: duplicate deliveries, transient failures and signature verification.
Providers guarantee at-least-once delivery, so a handler can be called more than once for the same event. This library makes sure an event is only applied once, retries transient errors with backoff, and sends anything that keeps failing to a dead-letter sink you control.

Install
Install straight from GitHub (the package builds itself on install):
npm install github:rovidev95/rovidev-webhooks-engine
# only if you use the Redis store:
npm install ioredisUsage
import { WebhookEngine, verifySignature, NonRetryableError } from "@rovidev/webhooks-engine";
const engine = new WebhookEngine({
maxAttempts: 5,
backoff: { baseMs: 250, maxMs: 10_000 },
deadLetter: async ({ event, error }) => {
await db.deadLetters.insert({ id: event.id, error });
},
});
engine.on<{ amount: number }>("payment.succeeded", async (event) => {
if (typeof event.payload.amount !== "number") {
throw new NonRetryableError("amount must be a number"); // won't retry
}
await fulfillOrder(event.payload.amount);
});Inside the HTTP handler:
if (!verifySignature(rawBody, signature, { secret: process.env.WEBHOOK_SECRET! })) {
return res.status(401).end();
}
const result = await engine.process({
id: body.id,
type: body.type,
payload: body.data,
receivedAt: Date.now(),
});
res.status(result.status === "dead_lettered" ? 500 : 200).json(result);result.status is one of processed, duplicate, in_progress, ignored
or dead_lettered.
Multiple instances
Behind a load balancer use the Redis store so the idempotency lock is shared.
It relies on SET key value PX <ttl> NX, which is atomic across processes.
import Redis from "ioredis";
import { WebhookEngine, RedisIdempotencyStore } from "@rovidev/webhooks-engine";
const engine = new WebhookEngine({
store: new RedisIdempotencyStore(new Redis(process.env.REDIS_URL!)),
});Signatures
// plain HMAC-SHA256 over the raw body
verifySignature(rawBody, signature, { secret });
// Stripe-style "t=...,v1=..." with replay protection (reject older than 5 min)
verifySignature(rawBody, header, { secret, toleranceSeconds: 300 });Verify against the raw request bytes, not the re-serialized JSON.
Stores
The IdempotencyStore interface has four methods (begin, complete, fail,
forget). Two implementations are included — in-memory and Redis — and you can
write your own over Postgres, DynamoDB, etc.
Local
npm install
npm test
npm run example # small Express server on :3000
# or
docker compose up --buildCustom work
Need a webhook/payment integration built or reviewed for your stack? Get in touch at rovidev.com.
License
MIT
