How to Test Stripe Webhooks Locally with the Stripe CLI
Local webhook testing is where you catch routing, signature, and handler bugs before they become production support tickets. The Stripe CLI can forward test events to your local server and print the signing secret your app must use for verification.
The goal is not only to see a 2xx response. The goal is to prove your app receives the event, verifies it, enqueues or runs the business effect, and updates local app state.
Start your local handler
Run your app locally on the port your webhook route expects. For example, if your Stripe webhook route is /stripe/webhook on port 3000, start the app first:
| |
Then forward Stripe events to that route:
| |
The CLI prints a webhook signing secret that starts with whsec_. Use that value for your local STRIPE_WEBHOOK_SECRET. It is not the same as the signing secret for your dashboard endpoint.
Trigger a checkout event
Use stripe trigger for common event fixtures:
| |
Your terminal running stripe listen should show the forwarded event. Your app logs should show that the event was verified and handled.
For invoice-credit flows, trigger:
| |
The generated fixture may not match your exact product or metadata, so use it first to validate route and signature behavior. Then create a real Checkout Session in test mode to validate your production-like metadata path.
Verify the signature path
A common local mistake is using the wrong signing secret. The secret from stripe listen verifies CLI-forwarded events. The secret from the Stripe Dashboard verifies events delivered to that registered dashboard endpoint.
Use environment variables explicitly:
| |
If verification fails locally, inspect your body parser before checking anything else. Stripe signature verification must use the raw request body.
Confirm the business effect
After the handler returns 2xx, check your app state directly:
- Was a job enqueued?
- Did the job run?
- Was access granted?
- Were credits added?
- Was the processed event ID recorded?
A 2xx response only confirms receipt. It does not prove fulfillment.
For a quick local assertion, query your database after the event:
| |
Adjust the database command for your stack. The useful habit is the same: verify the event record and the business state, not only the HTTP response.
Test duplicate delivery
Stripe can deliver the same event more than once. Your local tests should prove duplicate delivery does not double-apply effects.
Trigger or resend the same event, then check that your handler exits cleanly when the event ID already exists. For credit grants, also check that the invoice or payment ID cannot create duplicate credit rows.
If duplicate delivery is unsafe, fix idempotency before relying on replay in production.
Test the SendPromptly path
Once your handler is verified locally, add SendPromptly receipt/result calls around the same flow:
- Verify the Stripe signature.
- Enqueue or durably accept the work.
- Send
POST /api/v1/receipt. - Apply the business effect.
- Send
POST /api/v1/result.
Use deterministic idempotency keys during local retries so repeated test runs do not create confusing duplicate attempts.
Related guides
- Stripe signature verification errors — fix raw body and secret mismatch problems
- checkout.session.completed fired but app did not update — debug the most common checkout failure
- Stripe webhook idempotency guide — make local duplicate tests meaningful
Testing locally does not replace production monitoring, but it removes the most preventable webhook failures before customers are involved.