Stripe, GitHub, Shopify — point them all at one endpoint. JQ rules decide where each event goes: production, staging, or localhost:3000 via a built-in tunnel that never expires.
You find out when a customer complains, not when the event 404s.
ngrok free URLs rotate every 2 hours. You restart the tunnel, update the webhook URL in Stripe, and miss the 3 events that fired in between.
ERR tunnel session expired — 3 events lost
stripe: 402, 402, 402Stripe points at one URL, GitHub at another, Shopify at a third. Each has its own retry logic, error handling. One breaks — you find out days later.
WARN stripe endpoint → 502 (since 3d)
0/47 events deliveredA charge.failed event fired at 2am. Was the payload malformed? Did your handler throw? The provider doesn't store request bodies. You'll never know.
GET /events/evt_3f8a2c/body
404 — payload not retained by providerFrom POST to delivery in under 50ms. Here is the entire flow.
Give every provider a stable endpoint URL. Relayers resolves the target endpoint from an immutable public ID.
POST https://api.relayers.app/v1/webhooks/wep_1234567890abcdef1234567890abcdGive every provider a stable endpoint URL. Relayers resolves the target endpoint from an immutable public ID.
POST https://api.relayers.app/v1/webhooks/wep_1234567890abcdef1234567890abcdWrite a JQ expression. If it returns true, the rule fires. Match on body, headers, or query params.
.headers["x-github-event"] == "push"Write a JQ expression. If it returns true, the rule fires. Match on body, headers, or query params.
.headers["x-github-event"] == "push"Reshape the payload before it hits your service. Flatten nested objects, convert units, strip fields.
{event: .type, amount: .data.object.amount / 100}Reshape the payload before it hits your service. Flatten nested objects, convert units, strip fields.
{event: .type, amount: .data.object.amount / 100}Public URL, internal service, or localhost — all in the same rule set. Tunnels connect over WebSocket, no port forwarding.
wr tunnel --port 3000Public URL, internal service, or localhost — all in the same rule set. Tunnels connect over WebSocket, no port forwarding.
wr tunnel --port 3000Every event logged. Every delivery traced. Every payload inspectable.
Use a generated public webhook URL for providers and keep a friendly slug for internal organization. Each endpoint tracks its own rules, destinations, and delivery stats.
Full request and response bodies for every event. Filter by status, search by payload content, replay failed events with one click.
Every delivery attempt logged with HTTP status, response time, and body. See exactly why attempt 1 returned 500 and attempt 3 succeeded.
Write JQ, see input/output side by side. Test against real payloads before deploying. No guessing, no deploy-and-pray.
{event: .type, amount: .data.object.amount / 100, currency: .data.object.currency}{"type": "charge.succeeded", "data": {"object": {"amount": 2500, "currency": "usd"}}}{"event": "charge.succeeded", "amount": 25, "currency": "usd"}Every connected daemon: hostname, port, status, last heartbeat. Know instantly if your local environment is receiving events.
These are actual JQ rules you would write. Copy them.
Route WhatsApp Business API callbacks by message type. Text goes to your local chat handler during dev, media to a cloud processor, every message logged for compliance.
.messages[0].type == "text"Write a JQ expression. The input payload goes in, your custom schema comes out. No adapter code in your service.
{
"type": "charge.succeeded",
"data": {
"object": {
"id": "ch_1abc",
"amount": 4999,
"currency": "usd",
"customer": "cus_xyz",
"status": "succeeded"
}
}
}{
event: .type,
charge_id: .data.object.id,
amount: (.data.object.amount / 100),
currency: .data.object.currency,
customer: .data.object.customer
}{
"event": "charge.succeeded",
"charge_id": "ch_1abc",
"amount": 49.99,
"currency": "usd",
"customer": "cus_xyz"
}The middleware you would write yourself, already running on every event.
HMAC-SHA256, SHA-1, and SHA-512 validated on ingest. Invalid signatures return 401 before your rules even see the event.
Exponential backoff with jitter. Configure max attempts and interval per rule. A 500 on attempt 1 retries at 30s, 1m, 2m, 4m — up to your limit.
Set a dedup window per endpoint. Relayers extracts the idempotency key from the payload and discards duplicates before delivery fires.
Per-endpoint sliding window. Burst traffic queued in PostgreSQL-backed jobs, not dropped. When the window opens, events deliver in order.
Relayers is the only webhook router with native localhost tunnels and JQ-based routing. Others charge $39-$490/mo for less.
| Feature | Relayers | Hookdeck | Svix | Convoy |
|---|---|---|---|---|
| Receive + Route + Deliver | Send only | |||
| Built-in localhost tunnels | CLI add-on | |||
| JQ routing rules | ||||
| Payload transformations | JS only | |||
| Self-hostable | ||||
| Starting price | Free | $39/mo | $490/mo | $99/mo |
WebSocket tunnels to localhost. No ngrok, no expiring URLs. Run `wr tunnel --port 3000` and receive events locally.
One language for routing, filtering, and transformations. If you can write a jq pipe, you can configure Relayers.
Isolated tenant workspaces with scoped API keys. Each team gets its own endpoints, rules, and event history.
Sign up, create an endpoint, paste the URL into Stripe. That is it. No credit card required.