Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to test webhooks locally for Stripe

The Stripe CLI lets you forward webhook events to your local development server without deploying to a public URL. Run 'stripe listen --forward-to localhost:3000/webhook' to start receiving events locally. The CLI provides a temporary webhook signing secret and lets you trigger specific events for testing. This guide covers installation, event forwarding, triggering test events, and debugging webhook issues.

What you'll learn

  • How to install and authenticate the Stripe CLI
  • How to forward webhook events to your local server
  • How to trigger specific webhook events for testing
  • How to debug webhook signature verification issues
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read10 minutesStripe CLI v1.19+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

The Stripe CLI lets you forward webhook events to your local development server without deploying to a public URL. Run 'stripe listen --forward-to localhost:3000/webhook' to start receiving events locally. The CLI provides a temporary webhook signing secret and lets you trigger specific events for testing. This guide covers installation, event forwarding, triggering test events, and debugging webhook issues.

Testing Stripe Webhooks on Your Local Machine

During development, your webhook endpoint is running on localhost which Stripe cannot reach. The Stripe CLI bridges this gap by establishing a WebSocket connection to Stripe and forwarding events to your local server. It also provides a temporary webhook signing secret (whsec_) for signature verification. This is the recommended way to develop and test webhook handlers.

Prerequisites

  • Node.js 18 or later with your webhook server code ready
  • A Stripe account (test mode)
  • Homebrew (macOS), scoop (Windows), or apt (Linux) for CLI installation

Step-by-step guide

1

Install the Stripe CLI

Install the CLI using your platform's package manager.

typescript
1# macOS
2brew install stripe/stripe-cli/stripe
3
4# Windows (scoop)
5scoop bucket add stripe https://github.com/stripe/scoop-stripe-cli.git
6scoop install stripe
7
8# Linux (Debian/Ubuntu)
9curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg
10echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list
11sudo apt update && sudo apt install stripe
12
13# Verify installation
14stripe version

Expected result: The Stripe CLI is installed and outputs its version number.

2

Authenticate the CLI with your Stripe account

Log in to connect the CLI to your Stripe account. This opens a browser for authentication.

typescript
1# Log in to Stripe (opens browser for OAuth)
2stripe login
3
4# Or use an API key directly (useful for CI)
5stripe login --api-key sk_test_yourTestKey

Expected result: The CLI authenticates with your Stripe account and confirms the connection.

3

Forward webhooks to your local server

Start the webhook listener to forward all Stripe events to your local endpoint. The CLI provides a temporary signing secret.

typescript
1# Forward all events to your local server
2stripe listen --forward-to localhost:3000/webhook
3
4# Output:
5# Ready! Your webhook signing secret is whsec_abc123...
6# (use this secret for signature verification in your local server)
7
8# Forward only specific events:
9stripe listen --forward-to localhost:3000/webhook \
10 --events payment_intent.succeeded,payment_intent.payment_failed,customer.subscription.created

Expected result: The CLI connects to Stripe and begins forwarding events to localhost:3000/webhook. A signing secret is displayed.

4

Update your server to use the CLI signing secret

Use the whsec_ secret from the CLI output in your webhook endpoint for local development.

typescript
1// .env (for local development)
2STRIPE_WEBHOOK_SECRET=whsec_abc123... // From 'stripe listen' output
3
4// Your webhook handler stays the same:
5const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
6
7app.post('/webhook',
8 express.raw({ type: 'application/json' }),
9 (req, res) => {
10 const sig = req.headers['stripe-signature'];
11 let event;
12 try {
13 event = stripe.webhooks.constructEvent(
14 req.body,
15 sig,
16 process.env.STRIPE_WEBHOOK_SECRET // Uses CLI secret locally
17 );
18 } catch (err) {
19 return res.status(400).send(`Webhook Error: ${err.message}`);
20 }
21 console.log('Event received:', event.type);
22 res.json({ received: true });
23 }
24);

Expected result: Webhook signature verification works with the CLI-provided signing secret.

5

Trigger test events from the CLI

Use 'stripe trigger' to fire specific webhook events for testing. This creates real test objects in your Stripe test account.

typescript
1# Trigger a payment_intent.succeeded event
2stripe trigger payment_intent.succeeded
3
4# Trigger a subscription lifecycle
5stripe trigger customer.subscription.created
6
7# Trigger a dispute
8stripe trigger charge.dispute.created
9
10# Trigger an invoice payment failure
11stripe trigger invoice.payment_failed
12
13# Trigger a checkout session completion
14stripe trigger checkout.session.completed
15
16# List all available trigger events
17stripe trigger --list

Expected result: Each command creates test objects in Stripe and fires the corresponding webhook event to your local server.

6

Debug webhook delivery issues

The CLI shows real-time event delivery status. Use it to debug signature verification failures and handler errors.

typescript
1# The CLI shows delivery status for each event:
2# --> payment_intent.succeeded [evt_1ABC] -> localhost:3000/webhook [200]
3# --> payment_intent.payment_failed [evt_2DEF] -> localhost:3000/webhook [400]
4
5# Replay a specific event
6stripe events resend evt_1ABCxyz
7
8# View recent events in your account
9stripe events list --limit 10
10
11# Get details of a specific event
12stripe events retrieve evt_1ABCxyz
13
14# Common issues and fixes:
15# 400 error Signature verification failing Check STRIPE_WEBHOOK_SECRET matches CLI output
16# Connection refused Server not running Start your server on port 3000
17# Timeout Handler too slow Return 200 immediately, process async

Expected result: You can see event delivery status, replay events, and diagnose issues in real time.

Complete working example

local-webhook-test.js
1require('dotenv').config();
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3const express = require('express');
4const app = express();
5
6// Webhook endpoint
7app.post('/webhook',
8 express.raw({ type: 'application/json' }),
9 (req, res) => {
10 const sig = req.headers['stripe-signature'];
11 let event;
12
13 try {
14 event = stripe.webhooks.constructEvent(
15 req.body,
16 sig,
17 process.env.STRIPE_WEBHOOK_SECRET
18 );
19 } catch (err) {
20 console.error('Signature verification failed:', err.message);
21 console.error('Tip: Use the whsec_ from "stripe listen" output');
22 return res.status(400).send(`Webhook Error: ${err.message}`);
23 }
24
25 console.log(`\nReceived: ${event.type} (${event.id})`);
26
27 switch (event.type) {
28 case 'payment_intent.succeeded':
29 console.log(' Amount:', event.data.object.amount);
30 break;
31 case 'payment_intent.payment_failed':
32 console.log(' Error:', event.data.object.last_payment_error?.message);
33 break;
34 case 'customer.subscription.created':
35 console.log(' Sub ID:', event.data.object.id);
36 break;
37 case 'charge.dispute.created':
38 console.log(' Dispute:', event.data.object.id);
39 break;
40 default:
41 console.log(' (unhandled event type)');
42 }
43
44 res.json({ received: true });
45 }
46);
47
48app.use(express.json());
49
50const PORT = process.env.PORT || 3000;
51app.listen(PORT, () => {
52 console.log(`Webhook test server on port ${PORT}`);
53 console.log('\nSteps:');
54 console.log('1. Run: stripe listen --forward-to localhost:' + PORT + '/webhook');
55 console.log('2. Copy the whsec_ secret to your .env file');
56 console.log('3. Restart this server');
57 console.log('4. Run: stripe trigger payment_intent.succeeded');
58});

Common mistakes when testing webhooks locally for Stripe

Why it's a problem: Using the Dashboard webhook secret instead of the CLI's whsec_ for local testing

How to avoid: The CLI generates its own signing secret. Use the whsec_ displayed when you run 'stripe listen' in your local .env.

Why it's a problem: Forgetting to start the webhook server before running stripe listen

How to avoid: Start your server first (node server.js), then run stripe listen to forward events to it.

Why it's a problem: Closing the stripe listen terminal session while testing

How to avoid: Keep the stripe listen process running in a separate terminal. Events stop forwarding when you close it.

Why it's a problem: Not restarting the server after updating the webhook secret in .env

How to avoid: dotenv reads .env at startup. Restart your server after changing STRIPE_WEBHOOK_SECRET.

Best practices

  • Keep stripe listen running in a dedicated terminal during development
  • Use --events flag to filter only the events you are testing to reduce noise
  • Use stripe trigger to test each event handler individually
  • Use separate .env files for local (CLI whsec_) and production (Dashboard whsec_) webhook secrets
  • Test error scenarios: trigger payment_intent.payment_failed and charge.dispute.created
  • Run stripe events list to see recent events when debugging issues

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

How do I test Stripe webhooks on my local machine? I need to forward events to localhost without deploying. Show me how to use the Stripe CLI for webhook testing.

Stripe Prompt

Help me set up local Stripe webhook testing. I need to install the Stripe CLI, forward events to my local Node.js server, trigger test events, and debug signature verification. Show me the complete workflow.

Frequently asked questions

Do I need a public URL for local webhook testing?

No. The Stripe CLI creates a WebSocket tunnel to forward events to your local server. No public URL, ngrok, or port forwarding needed.

Is the CLI webhook secret different from the Dashboard secret?

Yes. The CLI generates a temporary signing secret each time you run stripe listen. Use the CLI secret for local development and the Dashboard secret for production.

Can I trigger custom webhook events?

stripe trigger supports common event types. For custom scenarios, create the objects via the API (e.g., create a PaymentIntent and confirm it) and the webhook will fire naturally.

How do I test webhook retries locally?

Return a non-2xx status from your handler. The CLI will show the failed delivery but does not automatically retry like Stripe's production infrastructure does.

Can multiple developers share one stripe listen session?

No. Each developer should run their own stripe listen session forwarding to their local server. Events from one session will not reach other sessions.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.