How to Test Stripe Webhooks Locally with the Stripe CLI

SendPromptly Team
3 min read

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:

1
npm run dev

Then forward Stripe events to that route:

1
stripe listen --forward-to localhost:3000/stripe/webhook

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:

1
stripe trigger checkout.session.completed

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:

1
stripe trigger invoice.paid

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:

1
2
export STRIPE_WEBHOOK_SECRET=whsec_from_stripe_listen
npm run dev

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:

1
2
sqlite3 dev.db \
  "select event_id, event_type, processed_at from processed_stripe_events order by processed_at desc limit 5;"

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:

  1. Verify the Stripe signature.
  2. Enqueue or durably accept the work.
  3. Send POST /api/v1/receipt.
  4. Apply the business effect.
  5. Send POST /api/v1/result.

Use deterministic idempotency keys during local retries so repeated test runs do not create confusing duplicate attempts.

Testing locally does not replace production monitoring, but it removes the most preventable webhook failures before customers are involved.