# 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:

```bash
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](https://expose.dev/#pricing) you can keep the same URL every time, so you only configure Stripe once:

```bash
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](https://expose.dev/docs/advanced-usage/request-plugins) 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](https://expose.dev/docs/client/catch-all).
