惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
博客园 - 司徒正美
Last Week in AI
Last Week in AI
Recent Announcements
Recent Announcements
Y
Y Combinator Blog
博客园 - 聂微东
M
MIT News - Artificial intelligence
博客园_首页
Jina AI
Jina AI
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
Vercel News
Vercel News
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
N
Netflix TechBlog - Medium
B
Blog
Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Stripe Webhook Testing: Local Development Guide
Digital Troubadour · 2026-06-04 · via DEV Community

Stripe webhooks notify your application about events—successful payments, failed charges, subscription changes, disputes. Getting them right matters. A missed webhook can mean unfulfilled orders or confused customers.

Testing webhooks locally used to be painful. Now Stripe has decent tooling. Here's how to use it effectively.

Setting Up the Stripe CLI

The Stripe CLI is your primary tool for local webhook testing. Install it first.

macOS:

brew install stripe/stripe-cli/stripe

Enter fullscreen mode Exit fullscreen mode

Windows:

scoop install stripe

Enter fullscreen mode Exit fullscreen mode

Linux:
Download from Stripe's releases page or use their apt/yum repos.

After installation, authenticate:

stripe login

Enter fullscreen mode Exit fullscreen mode

This opens a browser to link your Stripe account. You'll need to redo this periodically—the session expires.

Forwarding Webhooks to Localhost

Start the listener:

stripe listen --forward-to localhost:3000/webhooks/stripe

Enter fullscreen mode Exit fullscreen mode

You'll see output like:

Ready! Your webhook signing secret is whsec_abc123...

Enter fullscreen mode Exit fullscreen mode

Save that signing secret. You'll need it for signature verification. It's different from your dashboard webhook secret—this one is specific to the CLI session.

Set it in your environment:

export STRIPE_WEBHOOK_SECRET=whsec_abc123...

Enter fullscreen mode Exit fullscreen mode

Now Stripe CLI intercepts webhooks and forwards them to your local server.

Triggering Test Events

You can trigger specific events without making real payments:

stripe trigger payment_intent.succeeded

Enter fullscreen mode Exit fullscreen mode

Common events to test:

# Payment flow
stripe trigger payment_intent.succeeded
stripe trigger payment_intent.payment_failed
stripe trigger charge.refunded

# Subscriptions
stripe trigger customer.subscription.created
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted
stripe trigger invoice.paid
stripe trigger invoice.payment_failed

# Checkout
stripe trigger checkout.session.completed

Enter fullscreen mode Exit fullscreen mode

Each trigger sends a realistic webhook payload to your forwarded endpoint.

Signature Verification

Always verify webhook signatures. See Stripe's signature verification guide for full details. Here's the pattern in different languages:

Node.js:

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.log(`Webhook signature verification failed.`, err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      // Handle successful payment
      break;
    // ... other cases
  }

  res.json({received: true});
});

Enter fullscreen mode Exit fullscreen mode

Go:

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    payload, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading body", http.StatusBadRequest)
        return
    }

    event, err := webhook.ConstructEvent(
        payload,
        r.Header.Get("Stripe-Signature"),
        os.Getenv("STRIPE_WEBHOOK_SECRET"),
    )
    if err != nil {
        http.Error(w, "Signature verification failed", http.StatusBadRequest)
        return
    }

    switch event.Type {
    case "payment_intent.succeeded":
        // Handle
    }

    w.WriteHeader(http.StatusOK)
}

Enter fullscreen mode Exit fullscreen mode

Python:

import stripe
from flask import Flask, request

@app.route('/webhooks/stripe', methods=['POST'])
def stripe_webhook():
    payload = request.get_data()
    sig_header = request.headers.get('Stripe-Signature')

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, os.environ['STRIPE_WEBHOOK_SECRET']
        )
    except ValueError as e:
        return 'Invalid payload', 400
    except stripe.error.SignatureVerificationError as e:
        return 'Invalid signature', 400

    if event['type'] == 'payment_intent.succeeded':
        payment_intent = event['data']['object']
        # Handle

    return '', 200

Enter fullscreen mode Exit fullscreen mode

The Raw Body Problem

A common gotcha: signature verification requires the raw request body. If your framework parses JSON automatically before your handler runs, verification will fail.

In Express, use express.raw():

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), handler);

Enter fullscreen mode Exit fullscreen mode

In other frameworks, ensure you're reading the raw body before any JSON parsing middleware touches it. The Stripe docs on signature errors cover this in detail.

Debugging Failed Webhooks

When things go wrong:

1. Check the CLI output

The Stripe CLI shows request/response details:

2026-01-25 10:23:45   --> payment_intent.succeeded [evt_123...]
2026-01-25 10:23:45   <-- [400] POST http://localhost:3000/webhooks/stripe

Enter fullscreen mode Exit fullscreen mode

A 400 response usually means signature verification failed.

2. Check your logs

Add logging before and after signature verification to see where it fails.

3. Verify the secret

The CLI provides a session-specific secret. Make sure you're using the right one. The secret from your Stripe dashboard won't work with CLI-forwarded webhooks.

4. Check for body parsing issues

If you're getting "No signatures found matching the expected signature for payload" errors, you likely have a body parsing problem.

Testing Without the CLI

Sometimes you want to test with captured real payloads. Options:

Replay from Stripe Dashboard:
In your Stripe dashboard, go to Developers > Webhooks > your endpoint. You can resend any recent webhook delivery.

Use a webhook capture service:
Services like ThunderHooks act as a webhook inbox. Point Stripe at your ThunderHooks URL once, and every webhook is captured—even when your laptop is closed.

When you're ready to debug:

  1. Open your dashboard, see the exact payload Stripe sent
  2. Start ngrok or your preferred tunnel
  3. Replay the webhook to your tunnel URL
  4. Fix your bug, replay again—same webhook, no need to trigger another Stripe event

This is especially useful for debugging production issues. Capture real production webhooks, then replay them against your local code until you find the bug.

Mock the webhook:
For unit tests, don't call Stripe at all. Mock the webhook payload and test your handler logic directly.

Production Checklist

Before going live:

  • [ ] Webhook endpoint is HTTPS (required by Stripe)
  • [ ] Signature verification is enabled with production secret
  • [ ] Handler returns 2xx quickly (< 30 seconds)
  • [ ] Idempotency handling for duplicate deliveries
  • [ ] Error logging captures event ID for debugging
  • [ ] Retry handling for temporary failures
  • [ ] Critical events have monitoring/alerting

Common Events to Handle

At minimum, most Stripe integrations need:

Event When Action
checkout.session.completed Customer finishes checkout Fulfill order
payment_intent.succeeded Payment completes Record payment, send receipt
payment_intent.payment_failed Payment fails Notify customer, retry logic
customer.subscription.created New subscription Provision access
customer.subscription.deleted Subscription canceled Revoke access
invoice.payment_failed Subscription payment fails Dunning flow

See Stripe's webhook events documentation for the full list.

Conclusion

The Stripe CLI makes local webhook testing manageable. Set up forwarding, trigger events, verify signatures work, and test your handler logic.

The key is testing the unhappy paths too—failed payments, disputed charges, expired cards. Those are where webhook handling bugs tend to hide.

Resources