What you're doing and why it's painful
Stripe webhooks don't work on localhost — Stripe can't reach your machine from the internet. The common workaround is ngrok, but it requires a separate account, generates a random URL every time, and needs its own signing secret. The Stripe CLI solves all of this in one command.
The pain: finding the right install method, realising your production signing secret won't work locally (it's a different key), and figuring out how to trigger specific events without going through a real checkout flow.
Prerequisites
- Stripe account (test mode is fine)
- A local server running with a webhook handler endpoint (e.g.
localhost:3000/api/webhooks/stripe) - The Stripe CLI installed (covered below)
Step 1 — Install the Stripe CLI
macOS (Homebrew):
brew install stripe/stripe-cli/stripe
Windows (Scoop):
scoop bucket add stripe https://github.com/stripe/scoop-stripe-cli.git
scoop install stripe
Linux / manual install:
Download the latest binary from stripe.com/docs/stripe-cli and add it to your PATH.
Verify the install:
stripe --version
Step 2 — Authenticate the CLI
stripe login
This opens a browser window. Log in to your Stripe account and click Allow access. The CLI stores your credentials locally — you only need to do this once per machine.
To confirm it worked:
stripe whoami
Step 3 — Start forwarding events to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Replace 3000 with whatever port your server runs on, and the path with your actual webhook route.
The CLI prints something like:
> Ready! Your webhook signing secret is whsec_abc123def456...
Important: copy this local signing secret — it is different from the one in your Stripe Dashboard. You need it in Step 4.
The CLI stays running and forwards all webhook events from your Stripe account to your local server in real time. Leave this terminal open while you develop.
Step 4 — Use the local signing secret
In your local .env file, set the CLI secret (not your production secret):
STRIPE_WEBHOOK_SECRET=whsec_abc123def456...
If you use the production signing secret for local testing, signature verification will fail and every webhook will return a 400.
Your server code doesn't need to change — just the value of the environment variable.
Step 5 — Trigger test events
Open a second terminal and trigger any Stripe event:
stripe trigger checkout.session.completed
Other useful events to trigger:
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
Check your server logs — you should see the event arrive, the signature verify, and your handler run.
To see all available trigger names:
stripe trigger --help
Common errors and gotchas
No signatures found matching the expected signature for payload
You're using the wrong signing secret. The CLI prints a local secret (whsec_...) that is different from your production webhook secret. Use the CLI secret in your .env file.
stripe: command not found after install
The Stripe CLI binary isn't in your PATH. On macOS, Homebrew usually handles this automatically. On Linux, move the binary to /usr/local/bin/stripe or add its directory to PATH.
Server receives events but returns 400
Check your middleware. The webhook route must use express.raw({ type: 'application/json' }) — not express.json(). Parsing the body as JSON before signature verification corrupts it.
stripe listen times out or disconnects
The CLI maintains a persistent connection. If your network is flaky, it will reconnect automatically. If it doesn't, restart the command.
Events aren't arriving
Make sure your server is actually running on the port you specified. Run curl -X POST localhost:3000/api/webhooks/stripe manually to confirm the route exists and isn't returning a 404.