How to Test Stripe Webhooks Locally: 2 Methods
By HookCap Team
How to Test Stripe Webhooks Locally
Stripe sends webhooks for everything that matters: successful payments, failed charges, subscription changes, disputes. If your app depends on any of these events, you need a reliable way to test them during development — before your code hits production and a customer’s payment silently fails to process.
The problem is straightforward: Stripe needs to reach your server over the public internet, but your development server is running on localhost:3000. This guide covers two practical methods to bridge that gap, plus the gotchas that trip up most developers.
The Core Problem
When you create a webhook endpoint in the Stripe dashboard, you provide a URL like https://yourapp.com/webhooks/stripe. Stripe sends POST requests to that URL when events occur.
During local development, your server is not publicly accessible. You need one of two things:
- A tunnel that exposes your local server to the internet
- A cloud endpoint that captures webhooks so you can inspect them
Both approaches work. The right choice depends on what you are testing.
Method 1: Stripe CLI (Direct Forwarding)
The Stripe CLI has a built-in listen command that forwards webhook events directly to your local server. This is Stripe’s official approach and works well for basic testing.
Setup
Install the Stripe CLI:
# macOS
brew install stripe/stripe-cli/stripe
# Linux (Debian/Ubuntu)
apt-get install stripe
# Or download directly from https://stripe.com/docs/stripe-cli
Authenticate with your Stripe account:
stripe login
This opens your browser for OAuth. Once authenticated, the CLI stores your API key locally.
Forward Events to Your Local Server
Start listening and forwarding:
stripe listen --forward-to localhost:3000/webhooks/stripe
The CLI prints a webhook signing secret:
> Ready! Your webhook signing secret is whsec_abc123def456...
Important: Copy this signing secret and use it in your local environment. This is different from the signing secret in your Stripe dashboard — the CLI generates its own.
# .env.local
STRIPE_WEBHOOK_SECRET=whsec_abc123def456...
Trigger Test Events
In a separate terminal, trigger specific events:
# Trigger a checkout.session.completed event
stripe trigger checkout.session.completed
# Trigger a payment failure
stripe trigger invoice.payment_failed
# Trigger a subscription cycle
stripe trigger customer.subscription.updated
You will see the events flow through in both terminals — the CLI window shows the forwarded events, and your server logs show the incoming requests.
Limitations of the Stripe CLI Approach
The CLI works well for triggering individual test events, but has some friction points:
- Test data only. The CLI sends synthetic events with fake data. The payload structure is correct, but the IDs and amounts are generic. If you need to test with specific product configurations or pricing, you need to create real test-mode objects first.
- No payload inspection UI. Events scroll past in the terminal. If you need to examine a complex nested payload (say, a subscription update with proration line items), you are copying JSON from terminal output.
- One endpoint at a time. If multiple services in your stack consume Stripe webhooks, you need multiple CLI sessions or a local router.
- Session-based. The signing secret changes every time you restart
stripe listen. You need to update your env var each time.
Method 2: Cloud Endpoint (Capture and Inspect)
The alternative is to point Stripe at a persistent cloud URL that captures and displays every incoming webhook. You inspect payloads in a web UI, verify signatures, and optionally forward to your local server.
This is what tools like HookCap, webhook.site, and RequestBin are built for.
Step-by-Step with a Cloud Endpoint
1. Create an endpoint URL
Sign up for a webhook testing tool and get a unique endpoint URL. It will look something like:
https://hook.hookcap.dev/ep_a1b2c3d4e5
This URL is permanent (it does not expire after a session) and immediately starts accepting requests.
2. Configure Stripe to send events to your endpoint
In the Stripe Dashboard:
- Go to Developers > Webhooks
- Click Add endpoint
- Paste your cloud endpoint URL
- Select the events you care about (or select all for exploration)
For test mode, you can also set this via the API:
curl https://api.stripe.com/v1/webhook_endpoints \
-u sk_test_your_key: \
-d url="https://hook.hookcap.dev/ep_a1b2c3d4e5" \
-d "enabled_events[]"="checkout.session.completed" \
-d "enabled_events[]"="invoice.payment_failed" \
-d "enabled_events[]"="customer.subscription.updated"
3. Trigger real events in test mode
Now create actual test-mode transactions. Go through your checkout flow, create a subscription, or use the Stripe Dashboard to manually create a payment. Each event Stripe generates will appear at your cloud endpoint in real time.
4. Inspect payloads
The key advantage here: you get a full UI to inspect headers, body, and timing for every webhook delivery. You can:
- Expand nested JSON objects
- Compare payloads across different events
- See Stripe’s retry behavior when your endpoint returns errors
- Search through historical webhooks
5. Replay when needed
Found a bug in your handler? Fix the code, then replay the exact same webhook payload against your local server. No need to re-trigger the event in Stripe.
When to Use Which Method
| Scenario | Stripe CLI | Cloud Endpoint |
|---|---|---|
| Quick smoke test of a single event | Best choice | Works |
| Debugging complex subscription flows | Workable | Better (payload inspection) |
| Testing retry/failure behavior | Manual | Built-in |
| Team sharing a test endpoint | Not easy | Share the URL |
| CI/staging environments | Not applicable | Good fit |
Common Gotchas
These are the issues that show up repeatedly in Stripe webhook integrations, regardless of which testing method you use.
1. Signature Verification Fails
Stripe signs every webhook with a secret. Your server must verify this signature. The most common mistake:
// WRONG: body has already been parsed as JSON
app.use(express.json());
app.post('/webhooks/stripe', (req, res) => {
// req.body is already parsed -- signature verification will fail
const event = stripe.webhooks.constructEvent(
req.body, // This is an object, not a string
req.headers['stripe-signature'],
webhookSecret
);
});
// RIGHT: use raw body for signature verification
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, // This is a Buffer (raw bytes)
req.headers['stripe-signature'],
webhookSecret
);
}
);
The fix: ensure your webhook route receives the raw request body, not the parsed JSON. In Express, use express.raw() middleware on the webhook route specifically.
2. Event Ordering Is Not Guaranteed
Stripe does not guarantee that invoice.created arrives before invoice.payment_succeeded. Your handler must be idempotent and must not assume ordering.
Practical approach: use the event type to decide what to do, and always fetch the latest state from the Stripe API rather than relying solely on the webhook payload.
case 'invoice.payment_succeeded':
// Don't trust the webhook payload for current subscription status.
// Fetch the latest subscription state from Stripe.
const subscription = await stripe.subscriptions.retrieve(
event.data.object.subscription
);
await updateSubscriptionInDb(subscription);
break;
3. Responding Too Slowly
Stripe expects a 2xx response within 20 seconds. If your handler does heavy processing (sending emails, provisioning resources), Stripe considers it a failure and retries.
Solution: acknowledge the webhook immediately, then process asynchronously.
app.post('/webhooks/stripe', async (req, res) => {
const event = verifyAndParse(req);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process in the background
processWebhookEvent(event).catch(err => {
console.error('Webhook processing failed:', err);
});
});
4. Forgetting Test Mode vs. Live Mode
Stripe test mode and live mode have separate webhook endpoints and separate signing secrets. A webhook endpoint configured in test mode will not receive live events, and vice versa. Double-check which mode you are working in when things seem silent.
Why HookCap
If the cloud endpoint approach fits your workflow, HookCap is built specifically for this. A few things that matter for Stripe webhook testing:
- Permanent endpoint URLs that do not expire between sessions. Configure once in Stripe, keep testing.
- Real-time payload streaming via WebSocket. You see webhooks arrive instantly, without refreshing.
- One-click replay to re-send any captured webhook to your local or staging server. Useful when iterating on handler logic.
- Response mocking to simulate different server responses (200, 500, timeout) and test Stripe’s retry behavior.
Free tier available at hookcap.dev. Paid plans start at $12/month.
Summary
Testing Stripe webhooks locally comes down to two viable approaches:
- Stripe CLI for quick, individual event testing during development. Fast to set up, limited in inspection capabilities.
- Cloud endpoint for deeper debugging, payload inspection, replay testing, and team collaboration.
Most developers end up using both: the CLI for rapid iteration, and a cloud endpoint for complex integration testing and debugging production-like scenarios. Start with whichever solves your immediate problem, and add the other when you need it.