Webhook testing

Webhook testing on localhost

Run expose share http://localhost:3000 and paste the HTTPS URL it prints into Stripe, GitHub, Shopify or Paddle. Every webhook reaches the handler on your machine. You see each request in a dashboard and replay it while you fix your code.

curl -fsSL https://expose.dev/install.sh | sh irm https://expose.dev/install.ps1 | iex
Create free account

Test a webhook in three steps

  1. Share your app. Install Expose with the command above and start your app. Then run expose share http://localhost:3000 with your app's port, or expose share my-app.test for a Laravel Herd or Valet site. The first time, Expose opens your browser so you can sign in or create a free account.
  2. 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.
  3. 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.

The Expose request dashboard with a list of requests and the headers of the selected request
  • 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 push or invoice.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.

Questions

How do I test a webhook without running a server?

Run expose catch-all. You get a public HTTPS URL that answers every request with 200 OK, and each request shows up in the dashboard on http://127.0.0.1:4040. Paste the URL into your provider, trigger an event and read the payload. No app needs to run.

Is webhook testing with Expose free?

Yes. The free Hobby account needs no credit card and gives you a random URL on a server in Germany, with a time limit per connection. Expose Pro, $79 a year per user, adds a URL that stays the same, custom domains and no time limit.

Do webhook signatures still verify through Expose?

Yes. Expose forwards the raw body and headers such as Stripe-Signature, X-Hub-Signature-256 and Paddle-Signature unchanged, so you verify them with the endpoint's secret like in production. A replay sends the original signature again, so libraries that reject old timestamps, such as Stripe's with its five-minute default, can refuse a replay of an older event.

What happens to webhooks while Expose isn't running?

They don't reach you. A tunnel only answers while expose runs, so the provider records a failed delivery and Expose doesn't keep a copy. Start expose again and resend the event from the Stripe Dashboard or from Recent Deliveries on a GitHub webhook.

Can I test on my phone or the local network?

Yes. The dashboard shows a QR code that opens your shared site on a phone. expose share my-app.test --lan gives you a URL on your local network, for devices on the same network, and works with expose catch-all too. Providers on the internet still need the public URL from expose share.

One URL for every test

Keep your webhook endpoint configured with reserved subdomains, custom domains and automatic TLS. Available with Expose Pro.

Explore Expose Pro