Open Source · MIT License · Docker-ready

Webhook-to-Email

A tiny, production-grade webhook receiver. POST anything, get an email. Optional Slack fan-out, HMAC verification, single-attempt retry on 5xx. Stripe, GitHub, Cal.com, Typeform, internal cron jobs. One notification destination for the lot.

~200
lines of Node
~80MB
Docker image
1k req/s
one CPU core
~50MB
RSS at idle
MIT
license

Why this exists

Modern SaaS tools all emit webhooks. Stripe, GitHub, Cal.com, Typeform, Vercel, Linear, Sentry, Twilio. Useful events live in webhook payloads — invoice paid, deploy succeeded, booking made. Most of those events deserve at least an email or a Slack ping.

The conventional answer is Zapier or Make.com. Per-event pricing, vendor lock-in, opaque retries, surprise bills. The other answer is "build it in the SaaS that emitted the event" — except the SaaS rarely lets you template the email properly, and now your notification logic is scattered across twelve different products.

Webhook-to-Email is the third answer. 200 lines of Node. Docker one-liner. POST anything to /hooks/<source>, get an email. Drop a JS template in src/templates/ when you want a custom format. HMAC verification when you need it. Slack fan-out when you want it. Self-hosted, predictable, MIT-licensed.

Built-in features

Everything below works out of the box. Clone, set RESEND_API_KEY, run.

Per-source endpoints

POST /hooks/stripe, /hooks/github, /hooks/cal, /hooks/anything. Each source resolves its own template if one exists, or falls through to the default JSON formatter.

HMAC SHA-256 verification

Set WEBHOOK_SECRET and every request must include an X-Signature header. Both raw hex and sha256= prefixed formats are accepted, since Stripe, GitHub, and others emit different prefixes.

Per-source templates

Drop a JS file in src/templates/. Export a format(payload) function that returns { subject, text, html }. POST to /hooks/<source> and the template fires. Return null to fall through to the default formatter.

Resend for delivery

Resend is the cleanest transactional email API on the market. Verified domains, deliverability tracking, generous free tier. Single env var and you are sending.

Slack fan-out

Set SLACK_WEBHOOK_URL and the same payload is also POSTed to Slack as a formatted message. Email for the audit trail, Slack for the immediate signal.

Single retry on 5xx

Resend has occasional transient 5xx errors. One retry with 500ms backoff catches almost all of them. More retries belong in a queue layer.

No database, no state

Stateless by design. No migrations, no connection pool, no failures from database outages. If you need a queue, put one in front of this service.

Docker one-liner

docker run -d --env-file .env -p 3000:3000 webhook-to-email. The image is ~80MB on Alpine Node 20. docker-compose.yml ships in the repo for one-command deploys.

High throughput on tiny resources

Single-process Node, ~50MB RSS at idle, ~80MB under sustained load. Handles roughly 1,000 requests per second on one CPU core before Resend rate limits become the bottleneck.

Examples for the common sources

examples/ ships working curl invocations and template files for Stripe (invoice.paid, customer.subscription.created), GitHub (push, pull_request), Cal.com (booking created), Typeform (form response).

Tech stack

Node.js 20Express 4ResendDockerAlpine LinuxSlack Webhooksdocker-composeFly.io / Render / Railway

Architecture sketch

Stateless. No database. Logs to stdout. Trivially deployable.

┌─────────────────────────────────────────────────────────────┐
│  POST /hooks/:source                                        │
│    External service (Stripe, GitHub, Cal.com, ...)          │
│       │                                                     │
│       ▼ verify HMAC SHA-256  (if WEBHOOK_SECRET set)        │
│       │   X-Signature: sha256=<hex(hmac(body, secret))>     │
│       │                                                     │
│       ▼ load src/templates/<source>.js  (if exists)         │
│       │   format(payload) → { subject, text, html }         │
│       │   else: pretty-print JSON as default                │
│       │                                                     │
│       ▼ resend.emails.send({ from, to, subject, text, html })│
│       │   on 5xx → retry once with 500ms backoff            │
│       │                                                     │
│       ▼ if SLACK_WEBHOOK_URL set:                           │
│       │   POST formatted message to Slack incoming webhook  │
│       │                                                     │
│       ▼ 200 OK { ok: true }                                 │
└─────────────────────────────────────────────────────────────┘

Quick start

Local first, then Docker, then cloud. Same env vars throughout.

git clone https://github.com/sarmakska/webhook-to-email.git
cd webhook-to-email
npm install
cp .env.example .env
# Fill in RESEND_API_KEY and NOTIFY_EMAIL
npm start
# In another terminal:
curl -X POST http://localhost:3000/hooks/test \
  -H "Content-Type: application/json" \
  -d '{"hello": "world", "user": {"name": "Sarma"}}'

# Check your inbox. Email titled: "Webhook · test"
# Production: Docker
docker build -t webhook-to-email .
docker run -d --env-file .env -p 3000:3000 webhook-to-email

Use cases

What people actually run this for.

Single notification destination

Stop juggling 12 different webhook inboxes from 12 different SaaS tools. Route everything through this and into one email or Slack channel.

Stripe event summary

Stripe sends 100+ webhook types. Template the ones you care about (invoice.paid, subscription.created, payment_intent.failed) and ignore the rest.

Cal.com booking alerts

Forward every Cal.com booking to your personal email immediately. Customer details, time, service, on the device you actually check.

Internal cron job audit trail

Your nightly Postgres backup script POSTs success/failure to /hooks/cron. You get an email if anything goes wrong, no email if it works.

Open source · MIT

Use it. Fork it. Ship it.

MIT licensed. No strings attached. Pull requests welcome — SQS / Redis queue option, replay endpoint, multi-tenant routing are all on the roadmap.