← All guides
StripeJuly 12, 20263 min read

How to receive Stripe webhooks on your localhost (no ngrok)

Test Stripe webhooks locally with a tunnel URL that never expires. Point Stripe at Relayers, forward events to localhost, and filter with JQ — step by step.

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

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 DashboardDevelopers → Webhooks → Add endpoint, paste your Relayers ingest URL and select the events you care about, e.g.:

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:

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

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.