A small, reliable webhook relay for e-commerce integrations. Your store (or payment
provider, or marketplace) sends order.created, order.paid, stock.updated … to one URL; the
relay verifies the signature, stores the event, and delivers it to every system that needs it
(ERP, CRM, accounting, warehouse) with retries until each one confirms.
shop ──signed POST──▶ webhook-relay ──▶ ERP (retries with backoff)
│ SQLite ──▶ CRM (dead-letter after N attempts)
└─ outbox ──▶ accounting (manual replay via admin API)
Calling five systems directly from a checkout request is fragile: one slow or broken target
breaks the order flow, and a lost webhook means a missing invoice. The relay answers the sender
in milliseconds with 202 Accepted and takes over delivery.
- Authenticity. Inbound
X-Signature: t=…,v1=…(HMAC-SHA256 over timestamp + raw body), constant-time compare, 5-minute replay window. Outbound requests are signed the same way. - Exactly-once queueing. Events are de-duplicated per source by
X-Event-Id, the body'sid, or a hash of the body. Sender retries never create duplicate deliveries. - Durable outbox. Every (event, target) pair is a SQLite row, so a restart loses nothing.
- Smart retries. Exponential backoff with jitter; network errors,
5xx,408and429are retried, other4xxgo straight to the dead-letter queue. - Operations. Admin API to list deliveries, inspect errors, replay dead ones and see stats.
git clone https://github.com/bahriy-dev/ecommerce-webhook-relay.git
cd ecommerce-webhook-relay
cp sources.example.json sources.json # set your secrets and target URLs
ADMIN_TOKEN=change-me SIGNING_SECRET=change-me-too npm start
npm test # 14 testsSend a test event:
BODY='{"id":"ord_1001","type":"order.created","total":249900}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SHOP_SECRET" -hex | cut -d' ' -f2)
curl -X POST localhost:3000/hooks/shop -H "X-Signature: t=$TS,v1=$SIG" -d "$BODY"
# {"eventId":"ord_1001","queued":2,"duplicate":false}| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/hooks/:source |
signature | Receive an event from a configured source |
GET |
/deliveries?status=dead&limit=50 |
admin | List deliveries (pending, delivered, dead) |
GET |
/deliveries/:id |
admin | One delivery with attempts and last error |
POST |
/deliveries/:id/replay |
admin | Put a dead delivery back in the queue |
GET |
/stats |
admin | Counts per status |
GET |
/health |
— | Health check |
Admin routes need Authorization: Bearer $ADMIN_TOKEN.
Every outgoing request carries X-Event-Id, X-Event-Source, X-Delivery-Attempt and
X-Signature, so targets can verify the relay and de-duplicate on their side too.
| Variable | Default | Description |
|---|---|---|
ADMIN_TOKEN |
— | Required. Bearer token for the admin API |
SIGNING_SECRET |
— | Required. Secret used to sign outgoing requests |
SOURCES_FILE |
sources.json |
Sources, their secrets and target URLs |
DATABASE_PATH |
relay.db |
SQLite outbox |
MAX_ATTEMPTS |
8 |
Attempts before a delivery is dead |
BASE_DELAY_MS / MAX_DELAY_MS |
5000 / 3600000 |
Backoff start and cap |
TIMEOUT_MS |
10000 |
Per-request timeout |
BATCH_SIZE / POLL_INTERVAL_MS |
20 / 1000 |
Worker throughput |
src/
signature.ts HMAC signing and verification with replay protection
store.ts SQLite outbox: enqueue, due, mark delivered/failed, replay, stats
worker.ts delivery loop, retry classification, exponential backoff
app.ts inbound webhook endpoint and admin API
server.ts config, polling worker, graceful shutdown
test/ node:test: signatures, backoff, worker with fake fetch, HTTP API
MIT © Bahri YILMAZ