← Back to Blog
May 1, 202510 min read

Complete Guide to Stripe Webhook Setup and Testing

Payment processing and financial technology concept with credit cards and digital payment symbols
StripePaymentsWebhooksIntegration

Stripe webhooks are essential for handling asynchronous payment events in your SaaS application. This guide walks you through everything from initial setup to production deployment, including local testing strategies and security best practices.

Why Webhooks Matter

When a customer completes a payment, Stripe needs a way to notify your application. While you could poll the Stripe API, webhooks provide real-time, event-driven notifications that are more efficient and reliable. Webhooks handle critical events like successful payments, failed charges, subscription renewals, and refunds.

Without proper webhook handling, you might miss important events, leading to inconsistent application state, unpaid subscriptions, or customer service issues. This guide ensures you implement webhooks correctly from day one.

Setting Up Your Webhook Endpoint

First, create a webhook endpoint in your Express.js backend. This endpoint will receive POST requests from Stripe whenever events occur. Here's a basic implementation:

// server/routes/webhook.routes.js
import express from 'express';
import stripe from 'stripe';

const router = express.Router();
const stripeClient = stripe(process.env.STRIPE_SECRET_KEY);

// Important: This route must NOT use body-parser middleware
// Stripe needs the raw body to verify the signature
router.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;

  try {
    event = stripeClient.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    console.error('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;
      // Update your database, send confirmation email, etc.
      console.log('Payment succeeded:', paymentIntent.id);
      break;
    case 'customer.subscription.created':
      const subscription = event.data.object;
      // Activate user subscription in your database
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

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

export default router;

Notice the use of express.raw() middleware. This is crucial because Stripe needs the raw request body to verify the webhook signature. If you parse the body as JSON first, signature verification will fail.

Testing Locally with ngrok

Stripe needs a publicly accessible URL to send webhooks, but your local development server isn't accessible from the internet. ngrok creates a secure tunnel to your localhost, making it accessible to Stripe.

Install ngrok and start a tunnel:

# Install ngrok (macOS)
brew install ngrok

# Start tunnel to your local server (port 5000)
ngrok http 5000

# You'll get a URL like: https://abc123.ngrok.io
# Use this as your webhook endpoint in Stripe Dashboard

In the Stripe Dashboard, go to Developers → Webhooks and add your ngrok URL. Stripe will provide a webhook signing secret—add this to your .env file as STRIPE_WEBHOOK_SECRET.

Production Deployment with Cloudflare

For production, you'll use Cloudflare Tunnel (formerly Argo Tunnel) to securely expose your local backend. This is more reliable than ngrok and doesn't require exposing your server's IP address.

Set up Cloudflare Tunnel:

# Install cloudflared
# macOS: brew install cloudflared
# Linux: Download from cloudflare.com

# Authenticate
cloudflared tunnel login

# Create tunnel
cloudflared tunnel create diggabyte-api

# Configure tunnel (config.yml)
tunnel: diggabyte-api
credentials-file: /path/to/credentials.json

ingress:
  - hostname: api.diggabyte.com
    service: http://localhost:5000
  - service: http_status:404

# Run tunnel
cloudflared tunnel run diggabyte-api

Update your Stripe webhook endpoint to https://api.diggabyte.com/api/webhook and use the webhook secret from your production Stripe account.

Security Best Practices

  • Always verify webhook signatures: Never process webhook events without verifying the signature. This prevents malicious actors from sending fake events to your endpoint.
  • Use idempotency keys: Stripe events can be delivered multiple times. Use idempotency keys or check if you've already processed an event before taking action.
  • Log all webhook events: Keep detailed logs of received webhooks for debugging and auditing. Store event IDs to detect duplicates.
  • Handle errors gracefully: Return appropriate HTTP status codes. Return 200 for successful processing, 400 for invalid requests, and 500 for processing errors.

Common Webhook Events

Here are the most important webhook events you should handle:

payment_intent.succeeded

Fired when a payment is successfully completed. Update your database to mark the order as paid.

customer.subscription.created

New subscription activated. Grant the user access to premium features.

customer.subscription.deleted

Subscription cancelled. Revoke premium access and notify the user.

charge.refunded

Payment refunded. Update order status and inventory if applicable.

Testing Your Webhook Handler

Stripe provides a webhook testing tool in the Dashboard. You can send test events to your endpoint to verify everything works correctly. Additionally, use the Stripe CLI for local testing:

# Install Stripe CLI
# macOS: brew install stripe/stripe-cli/stripe
# Linux: See stripe.com/docs/stripe-cli

# Login
stripe login

# Forward webhooks to local server
stripe listen --forward-to localhost:5000/api/webhook

# Trigger test events
stripe trigger payment_intent.succeeded

Conclusion

Proper webhook implementation is critical for reliable payment processing. By following this guide, you'll have a secure, tested webhook handler that can handle production traffic. Remember to always verify signatures, handle idempotency, and log events for debugging.

Our Full-Stack SaaS template includes a complete webhook implementation with all these best practices built-in. Download it to see a production-ready example you can use as a starting point.

Ready to Ship Your SaaS?

Stop writing the same boilerplate code. Browse our production-ready SaaS templates — each includes authentication, Stripe billing, database setup, and deployment configs so you can launch in days instead of months.

Check out our straightforward pricing, see how we compare to ShipFast and MakerKit, or read the documentation to understand exactly what you get. Questions? Learn about our team and mission, or catch up on our engineering blog.