If you've ever built a Stripe integration, you know the pain: Stripe sends events (checkout.session.completed, invoice.paid, charge.refunded) to a public URL, but your handler is running on localhost:3000. The usual workarounds — spinning up ngrok, redeploying to a staging server for every change, or replaying events by hand — are slow and break your flow.
This guide shows how to receive real Stripe webhooks on your localhost with Relayers: a tunnel URL that never expires, a payload inspector, and JQ routing rules — so you can iterate on your webhook handler with production-shaped events.
What you'll build
You get a stable ingest URL to paste into Stripe, and every event is forwarded straight to your local handler. No redeploys, no rotating URLs.
Prerequisites
- A Stripe account (test mode is fine)
- Your webhook handler running locally (e.g.
localhost:3000) - The Relayers CLI or desktop app (download here)
1Create an endpoint in Relayers
An endpoint is the public address that receives webhooks. In the desktop app, open Endpoints → New endpoint, name it stripe, and copy its ingest URL:
https://api.relayers.app/v1/webhooks/wep_your_endpoint_id
Prefer the terminal? Log in and start the tunnel:
relayers login
relayers listen --forward localhost:3000
2Point Stripe at your endpoint
In the Stripe Dashboard → Developers → Webhooks → Add endpoint, paste your Relayers ingest URL and select the events you care about, e.g.:
checkout.session.completedinvoice.paidcharge.refunded
Stripe will now deliver every matching event to Relayers.
3Forward events to localhost
Add a routing rule on the endpoint with a tunnel destination pointing at your local handler:
- Destination: tunnel →
localhost:3000 - Filter:
{}(match everything) to start
Trigger a test event from the Stripe CLI or the Dashboard and watch it land in your terminal — and in the Relayers payload inspector, where you can see the exact headers and JSON body.
stripe trigger checkout.session.completed
4Route and filter with JQ (optional but powerful)
Not every event should hit the same handler. With Relayers rules you can filter by payload content using JQ-style expressions. For example, only forward paid invoices above $100:
{ "type": "invoice.paid", "data": { "object": { "amount_paid": { "$gte": 10000 } } } }
Or split traffic: send charge.refunded to a localhost:3001 refunds service and everything else to localhost:3000. Each rule runs top to bottom, first match wins.
5Transform the payload before delivery
Stripe payloads are large. If your local service only needs a few fields, add a transformation (jq) to the rule so your handler receives a lean object:
{ id: .id, type: .type, amount: .data.object.amount_paid, customer: .data.object.customer }
You can test the expression against a real captured event right in the app before saving it.
Verifying signatures
Stripe signs every webhook with a secret (whsec_...). Relayers forwards the original body and the Stripe-Signature header untouched, so your existing signature verification keeps working locally exactly as it does in production.
Going to production
The same endpoint and rules work in production — just change the rule's destination from tunnel to a public URL (your deployed service). Export your endpoint + rules as a JSON bundle and import them into another environment to keep dev, staging, and prod in sync.
Troubleshooting
- Event not arriving? Confirm the URL in Stripe matches your endpoint's ingest URL and that the tunnel is connected (the app shows a green "Connected" status).
- Handler returns non-2xx? Check the payload inspector — you can retry a delivery from the Events view once your handler is fixed.
- Signature check failing locally? Make sure you're using the signing secret from the Stripe endpoint you created, not a different one.
Wrap-up
With Relayers you get a stable Stripe webhook URL for localhost, full visibility into every payload, and JQ-based routing and transforms — so building and debugging Stripe integrations stops being a redeploy-and-pray loop.
Receive Stripe webhooks on your localhost free.