Test a webhook in three steps
-
Share your app. Install Expose with the command above and start your app.
Then run
expose share http://localhost:3000with your app's port, orexpose share my-app.testfor a Laravel Herd or Valet site. The first time, Expose opens your browser so you can sign in or create a free account. -
Paste the URL into your provider. Expose prints a public URL such as
https://abc123.eu-1.sharedwithexpose.com. Add your webhook route, for example/stripe/webhook, and enter it in Stripe under Developers → Webhooks, in GitHub under Settings → Webhooks, or as a notification destination in Paddle. - Trigger an event and watch it arrive. Make a test payment or push a commit, then open http://127.0.0.1:4040. You see the request, its headers and body, and what your app answered.
It works behind firewalls, VPNs and company networks. You open no ports and deploy nothing.
If your provider uses carrier-grade NAT, port forwarding can't work at all. The CGNAT check tells you in a few seconds.
Two ways to test a webhook
See the payload, no server needed
expose catch-all
You get a public URL that answers every request with 200 OK and shows it in the dashboard. Use it before you've written a handler, to learn what a provider sends.
Run your real handler
expose share http://localhost:3000
The webhook reaches your app and your app's response goes back to the provider. Body and signature headers arrive unchanged, so signature checks work like in production.
Read more about catch-all and sharing a local site.
Inspect and replay every webhook
The dashboard on http://127.0.0.1:4040 lists requests as they arrive. Click one to see its headers, body and your app's response.
- Replay. Fix your code, click Replay, and Expose sends the same request to your app again. You don't create another test order or subscription.
- Replay with changes. Edit the method, path, headers or body before you replay, to test an edge case your provider is hard to trigger.
-
Labelled webhooks. Built-in
request plugins label GitHub, Stripe and
Paddle Billing webhooks with their event, such as
pushorinvoice.paid.
A replay sends the original signature again. Stripe's libraries reject signatures older than five minutes by default, so raise the tolerance locally or resend the event from Stripe.
Keep the same webhook URL
On a free account the URL is random and changes when you reconnect, so you update it at your provider each time. With Expose Pro, $79 a year per user, you configure the provider once:
-
A reserved subdomain:
expose share http://localhost:3000 --subdomain=my-shop. -
A custom domain such as
hooks.example.com, set up with one CNAME record.
Let your coding agent read the webhooks
Connect the Expose MCP server at https://expose.dev/mcp to Claude Code,
Codex, ChatGPT or another agent. It can read the last 50 requests of each tunnel, for up to 30
minutes, with authorization headers, cookies, signatures and IP addresses removed. Ask "Why did
the last Stripe webhook fail?" and it reads the request, fixes your handler and checks the next
delivery. See Expose for coding agents.
ChatGPT can also subscribe to your tunnel and hear about a failing webhook as it happens, with MCP Events.
When a hosted request bin is enough
If you only want to look at one payload and can't install anything, a hosted bin like webhook.site is fine. You open a page and get a URL. It also keeps collecting while your laptop is closed, which a tunnel can't. Once you need your own code to answer, use a tunnel. See how Expose compares with webhook.site and RequestBin.
Step-by-step guides
Follow the guide for your provider: Stripe webhooks, Paddle webhooks, GitHub webhooks and Shopify apps and webhooks. Using a framework? See Laravel and Next.js.