Skip to content

Duplicate events ​

Providers retry deliveries (Paystack for 72 hours), and networks duplicate requests. Pass a store:

ts
interface IdempotencyStore {
  claim(id: string, ttlSeconds: number): boolean | Promise<boolean>; // true = first time we see it
  release?(id: string): void | Promise<void>;                        // called if onEvent throws
}

Redis

ts
const store = {
  claim: async (id, ttl) => (await redis.set(`wh:${id}`, '1', 'EX', ttl, 'NX')) === 'OK',
  release: async (id) => void (await redis.del(`wh:${id}`)),
};

Postgres

sql
CREATE TABLE webhook_events (id text PRIMARY KEY, seen_at timestamptz NOT NULL DEFAULT now());
ts
const store = {
  claim: async (id) => (await sql`INSERT INTO webhook_events (id) VALUES (${id}) ON CONFLICT DO NOTHING RETURNING id`).length > 0,
  release: async (id) => void (await sql`DELETE FROM webhook_events WHERE id = ${id}`),
};

event.id is provider:providerEventType:providerReference[:status], so a status change (e.g. Flutterwave pending → successful) is processed, but a retry of the same status is not. MemoryIdempotencyStore is for development only. Also make your fulfilment idempotent (e.g. "mark order paid if not already paid").

MIT Licensed