ideas.
September 06, 2026 3 min read apidev-toolsautomation

Stop losing inbound webhooks

A small ingress service that accepts, verifies, and stores every inbound webhook, then redelivers to your app until it actually succeeds.

The idea

A single-binary service that sits in front of your application and owns the job of receiving webhooks. Providers post to it, not to you. It verifies the signature, writes the raw payload to durable storage, returns 200 immediately, and then delivers the event to your app on its own schedule — retrying with backoff, deduplicating replays, and keeping every attempt on record. When your app was down for forty minutes, nothing is lost; the queue drains when it comes back.

Why build this

Every product that integrates Stripe, GitHub, Shopify, Twilio, or Slack ends up writing the same fragile handler: parse, verify, do real work, hope it finishes before the provider's timeout. It usually holds up until the first deploy that takes ninety seconds, or the first payload that triggers a slow database call, and then events silently vanish. Providers vary wildly in how forgiving they are — Stripe retries for three days, others give you three attempts and move on — so the failure mode is inconsistent and hard to reason about.

Big platforms solve this internally with a queue in front of every handler. Small teams skip it because the plumbing is boring, then spend an afternoon reconciling payments by hand. There is room for a self-hostable thing that does exactly this one job well, with no account, no per-event pricing, and no vendor sitting in the middle of your payment events.

Stack sketch

  • Ingress: Go with net/http and chi. Handlers do parse, verify, persist, ack — no business logic, so p99 stays under 10ms.
  • Storage: Postgres for event metadata and delivery attempts; payload bodies over 64KB spill to any S3-compatible bucket (MinIO locally, R2 in production).
  • Queue: Postgres itself via SELECT ... FOR UPDATE SKIP LOCKED. Adding Redis for this is a second thing to operate for no gain at this scale.
  • Signature verification: per-provider adapters for the four schemes that cover most of the market — Stripe's t=/v1= timestamped HMAC, GitHub's X-Hub-Signature-256, Shopify's base64 HMAC, and Slack's signing secret. A generic HMAC adapter with configurable header and encoding handles the long tail.
  • Config: one YAML file mapping source path to secret, destination URL, and retry policy. No admin database, no UI state.
  • CLI: wh tail streams live events, wh replay <id> redelivers one, wh replay --since 2h --failed drains a backlog.

Scope for v1

  • In: HTTP ingress, the four signature adapters plus generic HMAC, durable storage, at-least-once delivery with exponential backoff and jitter, idempotency keys derived from the provider's event ID, a /events JSON API, and the CLI.
  • In: a Docker image and a single docker compose up that gets you Postgres and the service.
  • Out: a web dashboard. Ship the JSON API and CLI first; the UI is a weekend after people are using it.
  • Out: transformation, filtering, and fan-out to multiple destinations. Those turn this into an integration platform, which is a different product.
  • Out: multi-tenancy and hosted billing.

Where it could go

The obvious next step is a local development mode: point a provider at the hosted ingress, run wh forward localhost:3000, and get every event tunneled to your laptop with full replay of anything that happened while you were at lunch. That replaces the ngrok-plus-Stripe-CLI dance most teams do today, and it is the feature that gets developers to install it before they have a production problem.

After that, the recorded event log becomes useful on its own. Every payload you have ever received, searchable by provider and type, is a test fixture library — export a week of real Stripe events and replay them against a branch. A hosted tier is plausible for teams that do not want another Postgres, but the self-hosted core should stay complete rather than crippled.

Watch out for

Signature verification has to happen on the exact raw bytes received, before any middleware touches the body — a JSON round-trip or a stray gzip decode will break Stripe and GitHub in ways that only show up in production. And be explicit that delivery is at-least-once: if you promise exactly-once, users will skip idempotency in their handlers and blame you for the duplicate charge.