Point Stripe, GitHub, or any provider at one URL and receive their webhooks on any localhost port you choose — through a native desktop app. No terminal, no expiring tunnel URLs. Inspect every payload, route by rules, ship faster.
macOS · Windows · Linux — or use the open-source CLI. Free forever tier, no credit card.
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.
Other tools point a raw tunnel at your machine. Relayers gives you a persistent URL, a native app, a payload inspector, and routing rules — built for webhooks, not generic traffic.
| Feature | Relayers | ngrok | Smee.io | webhook.site |
|---|---|---|---|---|
| Receive webhooks on localhost | View only | |||
| URL that never expires | Rotates (free) | Temporary | ||
| Native desktop app | ||||
| Payload inspector | Basic | |||
| Routing rules (JQ) | ||||
| Route to different local ports | ||||
| Retries + delivery history | Limited | |||
| Open-source CLI / self-host | Paid | |||
| Price to start | Free forever | Free (limited) | Free | Free (temp) |
Your URL and your local tunnel stay put across restarts, idle time, and network blips — no rotating ngrok links, no re-pasting webhook URLs into Stripe.
See every webhook land, expand the payload, and route it — in a native macOS, Windows, or Linux app. The open-source CLI is still there when you want it.
Match on any field with JQ and send each event where it belongs: this local port, that service, production, or staging. ngrok and Smee only forward blindly.
Download the app, sign in, and point Stripe or GitHub at your URL. The tunnel stays up — no re-pasting, no credit card.
macOS · Windows · Linux — or the open-source CLI. Free forever tier.