Test Stripe webhooks on localhost
#

Stripe sends webhooks to a public HTTPS URL, and your development server only listens on localhost. Expose gives your local server a public URL, shows you every event Stripe sends, and lets you replay an event without creating another test payment.

1. Share your local server
#

Start your application, then share the port it listens on:

expose share http://localhost:3000

Expose prints a public URL such as https://abc123.eu-1.sharedwithexpose.com. If you use Laravel Herd or Valet, run expose in the project directory instead.

With Expose Pro you can keep the same URL every time, so you only configure Stripe once:

expose share http://localhost:3000 --subdomain=my-shop

2. Add the URL as a webhook endpoint in Stripe
#

  1. Open the Stripe Dashboard in test mode (or a sandbox) and go to Developers → Webhooks.
  2. Add an endpoint (Stripe also calls it an event destination) and enter your Expose URL followed by your webhook route, for example https://my-shop.eu-1.sharedwithexpose.com/stripe/webhook.
  3. Select the events you need, such as checkout.session.completed and invoice.paid.
  4. Copy the endpoint's signing secret. It starts with whsec_.

3. Verify the signature in your app
#

Put the signing secret in your environment, for example STRIPE_WEBHOOK_SECRET=whsec_..., and let Stripe's library verify the Stripe-Signature header with it. Verification needs the raw request body, so read the body before a JSON parser changes it.

Expose forwards the request body and the Stripe-Signature header unchanged, so signature verification works the same as in production.

4. Trigger an event and inspect it
#

Make a test payment, or resend an event from the Stripe Dashboard. Then open the Expose dashboard:

http://127.0.0.1:4040

You see every request with its headers, body and your application's response. The built-in Stripe request plugin labels each request with its event type, so you can find invoice.paid at a glance.

5. Replay instead of paying again
#

When your handler has a bug, fix the code and click Replay on the request in the dashboard. Expose sends the same event to your local server again, so you don't need another test order. You can also change the request before replaying it.

Stripe's libraries reject signatures that are older than a few minutes (the default tolerance is five minutes). When you replay an older event, raise the tolerance in your local environment or resend the event from the Stripe Dashboard to get a fresh signature.

Troubleshooting
#

  • Signature verification fails. Make sure you use the signing secret of this endpoint, not your API key and not the secret from the Stripe CLI, and that you verify against the raw body.
  • Stripe reports a timeout. Stripe expects a 2xx response quickly. Return the response first and do slow work in a queue.
  • The URL changed. Free Expose URLs are random on each connection. Use --subdomain with Expose Pro, or update the endpoint in Stripe.
  • No endpoint in your app yet? Run expose catch-all to get a URL that accepts anything and shows the payloads in the dashboard. See Catch-All.