Minimal Express demo for testing Stripe webhooks locally with the Stripe CLI. Companion code to the walkthrough:
- 📝 Written walkthrough: How to Test Stripe Webhooks Locally — Stripe CLI, Replay, Logs
- 📺 Video walkthrough: link added after upload
What's demonstrated: forward real Stripe test events to localhost via the Stripe CLI, verify signatures against the raw request body, replay events by ID after a code fix, and prove idempotency by skipping duplicate event IDs.
- Node.js 20+
- Stripe CLI installed and logged in (
stripe login) - A Stripe test account (no real charges — everything runs in test mode)
# 1. Install
cd server
npm install
cp ../.env.example .env
# 2. In a second terminal — start the Stripe CLI listener
stripe listen --forward-to "http://localhost:8000/api/stripe-webhook-dev"
# Copy the printed whsec_... into server/.env as STRIPE_WEBHOOK_SECRET_DEV
# 3. Back in the first terminal — start the server
npm run dev
# 4. In a third terminal — trigger a test event
stripe trigger payment_intent.succeededYou should see a line like [stripe] payment_intent.succeeded evt_... in the server log.
The whole workflow boils down to four commands. Full list in commands.sh.
# listen
stripe listen --forward-to "http://localhost:8000/api/stripe-webhook-dev"
# trigger
stripe trigger payment_intent.succeeded
# replay (paste the evt_... ID from the listener output)
stripe events resend evt_xxx
# replay again — handler should skip the duplicate
stripe events resend evt_xxxserver/server.js— Express app withPOST /api/stripe-webhook-dev. Verifies the Stripe signature against the raw body, logsevent.type event.id, and keeps an in-memory dedupe set so you can demo idempotency.server/.env.example— copy toserver/.envand paste thewhsec_...printed bystripe listen.commands.sh— every command shown on screen, in order.snippets/handler.ts— the handler-shape snippet shown in the video.snippets/checklist.md— the pre-production checklist (five things to verify before pointing Stripe at production).
Local webhook testing uses a different signing secret than production — the one printed by the Stripe CLI, not the one on the Stripe dashboard. Mixing them on the same route is the most common source of "signature verification failed" pain. This demo uses a dedicated /api/stripe-webhook-dev route to keep that boundary obvious.
MIT — copy freely.