Building payment infrastructure requires reliable communication between your application and Stripe. While APIs let you request information from Stripe, webhooks enable Stripe to notify your application when events occur—from successful payments to subscription changes.
Stripe webhooks are HTTP callbacks that deliver real-time notifications about events in your Stripe account. When a customer completes a payment, disputes a charge, or a subscription trial ends, Stripe sends an HTTP POST request to your configured endpoint with detailed event data.
This guide covers everything you need to implement Stripe webhooks effectively: setting up endpoints, handling critical payment and subscription events, implementing security best practices, and testing your integration.
How Stripe Webhooks Work
Stripe webhooks follow the standard webhook pattern: when an event occurs in your Stripe account, Stripe sends an HTTPS POST request to your webhook endpoint containing event data in JSON format.
The Webhook Flow
- Event occurs: A customer completes a payment, subscription renews, or invoice fails.
- Stripe creates event object: Stripe generates an Event object with details about what happened.
- HTTP POST sent: Stripe sends a POST request to your webhook URL with the event data.
- Your application responds: Your endpoint processes the event and returns a 2xx status code.
- Retry on failure: If your endpoint doesn't respond with 2xx, Stripe retries the webhook.
Event Object Structure
Every Stripe webhook contains an Event object with this structure:
{
"id": "evt_1234567890",
"object": "event",
"api_version": "2024-11-20",
"created": 1732800000,
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_1234567890",
"amount": 2000,
"currency": "usd",
"status": "succeeded",
"customer": "cus_1234567890"
}
}
}
The type field indicates which event occurred (e.g., payment_intent.succeeded), while the data.object contains the full resource that triggered the event.
Setting Up Stripe Webhooks
Configure webhook endpoints through the Stripe Dashboard or API to start receiving event notifications.
Dashboard Configuration
- Navigate to Developers > Webhooks in your Stripe Dashboard.
- Click Add endpoint.
- Enter your webhook URL (must be HTTPS in production).
- Select events to listen for (or select "all events" during development).
- Save and note your webhook signing secret.
Endpoint Requirements
Your webhook endpoint must:
- Accept POST requests
- Parse JSON request body
- Verify webhook signatures
- Return 2xx status code within 5 seconds
- Process events idempotently (handle duplicates)
Basic Endpoint Implementation
// Express.js example
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} 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;
handlePaymentSucceeded(paymentIntent);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
handleSubscriptionCreated(subscription);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
// Return success response
res.json({received: true});
});
Payment Events
Payment-related webhooks notify you about charge and payment intent lifecycle changes. These events are critical for fulfilling orders and handling payment failures.
Successful Payment Events
payment_intent.succeeded: Sent when a payment is successfully processed.
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 5000,
"currency": "usd",
"status": "succeeded",
"customer": "cus_xyz789",
"payment_method": "pm_card123"
}
}
}
Use this event to:
- Fulfill orders and deliver digital goods
- Send payment confirmation emails
- Update user account status
- Record successful transactions in your database
charge.succeeded: Sent when a charge is successfully created. Similar to payment_intent.succeeded but at the Charge level.
Failed Payment Events
payment_intent.payment_failed: Sent when a payment attempt fails.
{
"type": "payment_intent.payment_failed",
"data": {
"object": {
"id": "pi_abc123",
"amount": 5000,
"status": "requires_payment_method",
"last_payment_error": {
"code": "card_declined",
"message": "Your card was declined."
}
}
}
}
Handle failed payments by:
- Notifying customers about payment failures
- Providing retry instructions or payment method update link
- Logging failure reasons for analytics
- Escalating to support team for high-value transactions
Refund Events
charge.refunded: Sent when a charge is refunded, either fully or partially.
{
"type": "charge.refunded",
"data": {
"object": {
"id": "ch_abc123",
"amount": 5000,
"amount_refunded": 5000,
"refunded": true
}
}
}
Subscription Events
Subscription webhooks track the complete lifecycle of recurring billing, from trial starts to cancellations. For SaaS businesses, these events are essential for access control and customer communication.
Subscription Creation
customer.subscription.created: Sent when a customer subscribes to a plan.
{
"type": "customer.subscription.created",
"data": {
"object": {
"id": "sub_abc123",
"customer": "cus_xyz789",
"status": "active",
"current_period_end": 1735689600,
"items": {
"data": [{
"price": {
"id": "price_abc123",
"product": "prod_xyz789"
}
}]
}
}
}
}
Actions to take:
- Grant access to subscription features
- Send welcome email with account details
- Set up usage tracking or limits
- Record subscription start date
Trial Events
customer.subscription.trial_will_end: Sent 3 days before a trial ends.
{
"type": "customer.subscription.trial_will_end",
"data": {
"object": {
"id": "sub_abc123",
"status": "trialing",
"trial_end": 1735603200
}
}
}
This critical event lets you:
- Remind users to add payment method
- Highlight product value to encourage conversion
- Offer limited-time discounts
- Prevent churn by addressing concerns
You can automate trial-ending notifications to ensure no customer falls through the cracks during this crucial conversion moment.
Subscription Updates
customer.subscription.updated: Sent when subscription details change (plan upgrade/downgrade, payment method update, etc.).
Handle this event to:
- Adjust feature access based on new plan
- Calculate prorated charges or credits
- Update billing information in your database
- Notify customer of changes
Subscription Cancellation
customer.subscription.deleted: Sent when a subscription is canceled and ends.
{
"type": "customer.subscription.deleted",
"data": {
"object": {
"id": "sub_abc123",
"status": "canceled",
"canceled_at": 1732800000
}
}
}
Critical actions:
- Revoke access to subscription features
- Send cancellation confirmation
- Trigger win-back campaigns
- Archive user data according to retention policy
Invoice Events
Invoice webhooks handle billing cycles, payment collection, and dunning management. These events are crucial for maintaining healthy subscription revenue.
Invoice Payment Failed
invoice.payment_failed: Sent when automatic payment collection fails for an invoice.
{
"type": "invoice.payment_failed",
"data": {
"object": {
"id": "in_abc123",
"customer": "cus_xyz789",
"amount_due": 2000,
"attempt_count": 1,
"next_payment_attempt": 1732886400
}
}
}
This event requires immediate action:
- Alert customer about failed payment
- Provide payment method update link
- Implement escalation workflow (retry → suspend → cancel)
- Track failed payment metrics
Automate invoice failure handling to notify customers immediately and escalate to your billing team after repeated failures.
Invoice Payment Succeeded
invoice.payment_succeeded: Sent when invoice payment succeeds.
Use this to:
- Send payment receipt to customer
- Extend subscription period
- Reset payment failure counters
- Update accounting records
Invoice Upcoming
invoice.upcoming: Sent 1 day before invoice is created for upcoming billing period.
{
"type": "invoice.upcoming",
"data": {
"object": {
"customer": "cus_xyz789",
"amount_due": 2000,
"next_payment_attempt": 1733097600
}
}
}
Proactive communication opportunities:
- Remind customers of upcoming charge
- Allow time to update payment method
- Preview invoice amount for transparency
- Reduce involuntary churn
Webhook Security Best Practices
Stripe webhooks contain sensitive customer and payment data. Implement security measures to prevent unauthorized access and ensure data integrity.
Verify Webhook Signatures
Every Stripe webhook includes a Stripe-Signature header containing a cryptographic signature. Always verify this signature before processing events.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
// Verify signature
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
endpointSecret
);
} catch (err) {
console.log(`Signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
The constructEvent method validates that the webhook came from Stripe and hasn't been tampered with. Never skip signature verification in production.
Implement Idempotency
Stripe may send the same webhook multiple times (network issues, retries). Design your webhook handler to process events idempotently.
async function handlePaymentSucceeded(paymentIntent) {
// Check if already processed
const existing = await db.payments.findOne({
stripe_payment_intent_id: paymentIntent.id
});
if (existing) {
console.log(`Payment ${paymentIntent.id} already processed`);
return;
}
// Process payment and mark as complete
await db.payments.create({
stripe_payment_intent_id: paymentIntent.id,
amount: paymentIntent.amount,
status: 'completed',
processed_at: new Date()
});
}
Use the event ID (event.id) or resource ID (payment_intent.id) to track processed events and prevent duplicate actions.
Use HTTPS Endpoints
Stripe requires HTTPS for production webhook endpoints. This encrypts webhook data in transit, preventing eavesdropping or tampering.
For local development, use the Stripe CLI to forward webhooks to your local HTTP endpoint securely.
Respond Quickly
Return a 2xx status code within 5 seconds, before performing complex operations:
app.post('/webhooks/stripe', async (req, res) => {
// Verify signature
const event = verifyWebhook(req);
// Immediately return success
res.json({received: true});
// Process event asynchronously
processEventAsync(event).catch(err => {
console.error(`Error processing event ${event.id}:`, err);
});
});
If your endpoint times out, Stripe will retry the webhook, potentially causing duplicate processing.
Testing Stripe Webhooks
Test webhooks thoroughly before going to production using Stripe's testing tools and local development workflow.
Stripe CLI for Local Testing
The Stripe CLI forwards webhook events from Stripe to your local development server:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login to your Stripe account
stripe login
# Forward webhooks to local endpoint
stripe listen --forward-to localhost:3000/webhooks/stripe
The CLI outputs a webhook signing secret for local testing. Use this secret in your local environment variables.
Trigger Test Events
Send test webhook events directly from the CLI:
# Trigger successful payment
stripe trigger payment_intent.succeeded
# Trigger subscription creation
stripe trigger customer.subscription.created
# Trigger invoice payment failure
stripe trigger invoice.payment_failed
Test Event IDs
Stripe provides specific test event IDs that trigger webhooks in test mode:
// Create test PaymentIntent that will trigger webhook
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: 'usd',
payment_method: 'pm_card_visa',
confirm: true
});
Use test cards like 4242 4242 4242 4242 for successful payments and 4000 0000 0000 0002 for declined cards to generate different webhook scenarios.
Monitor Webhook Deliveries
Check webhook delivery status in the Stripe Dashboard under Developers > Webhooks. This shows:
- Recent webhook attempts (successful and failed)
- Response status codes from your endpoint
- Response time
- Retry attempts
Failed webhooks can be manually retried from the dashboard for debugging.
Handling 200+ Stripe Events
Stripe generates over 200 different webhook event types. While you don't need to handle every event, understanding event categories helps you build robust integrations.
Event Naming Convention
Stripe webhook events follow the pattern resource.action:
payment_intent.succeeded- PaymentIntent succeededcustomer.subscription.deleted- Subscription deletedinvoice.payment_failed- Invoice payment failed
Common Event Categories
Checkout Events: checkout.session.completed, checkout.session.expired
Customer Events: customer.created, customer.deleted, customer.source.expiring
Dispute Events: charge.dispute.created, charge.dispute.closed
Fraud Events: radar.early_fraud_warning.created, radar.early_fraud_warning.updated
Payout Events: payout.paid, payout.failed
Event Filtering
Configure which events your endpoint receives in the Stripe Dashboard to reduce unnecessary webhook traffic and processing overhead.
For development, enable all events to observe the full Stripe event ecosystem. For production, subscribe only to events your application actively handles to minimize resource usage.
Automating Stripe Webhooks with Workflows
While custom webhook endpoints provide full control, they require significant development and maintenance effort. For common Stripe notification workflows, automation platforms can eliminate custom code entirely.
No-Code Workflow Automation
MagicBell's Stripe integration automatically handles webhook events and triggers notification workflows without writing webhook handlers. Simply connect your Stripe account and configure notification templates.
For example, to notify customers about trial endings:
{
"key": "integration.stripe.customer.subscription.trial_will_end",
"steps": [
{
"command": "broadcast",
"notification": {
"title": "Your trial ends in 3 days",
"content": "Add a payment method to continue your subscription",
"recipients": ["{{ customer.email }}"],
"channels": ["email", "in_app"]
}
}
]
}
The workflow automatically triggers when Stripe sends the customer.subscription.trial_will_end webhook, delivering notifications through multiple channels without custom code.
Combining Custom Code and Automation
Use both approaches strategically:
Custom webhooks for core business logic:
- Order fulfillment
- Inventory management
- Internal system updates
- Complex custom workflows
Workflow automation for customer notifications:
- Payment confirmations
- Subscription status changes
- Trial reminders
- Invoice notifications
This hybrid approach reduces development time while maintaining flexibility for complex requirements.
Production Deployment Checklist
Before deploying Stripe webhooks to production, verify these critical items:
Security
- Webhook signature verification implemented
- HTTPS endpoint with valid SSL certificate
- Environment variables for webhook secret (not hardcoded)
- IP allowlist configured if applicable
- Rate limiting on webhook endpoint
Reliability
- Idempotent event processing (handles duplicates)
- Async processing for long-running operations
- Error handling and logging
- Database transactions for critical updates
- Monitoring and alerting for failed webhooks
Testing
- All critical event handlers tested with Stripe CLI
- Edge cases covered (duplicate events, malformed data)
- Load testing for expected webhook volume
- Rollback plan if webhook processing fails
Monitoring
- Webhook delivery monitoring in Stripe Dashboard
- Application logs for webhook processing
- Alerts for webhook failures or timeouts
- Metrics tracking (processing time, error rates)
Troubleshooting Common Issues
Webhook Not Receiving Events
Check endpoint URL: Verify the URL in Stripe Dashboard matches your server endpoint exactly.
Confirm HTTPS: Production webhooks require HTTPS. Local development can use Stripe CLI forwarding.
Review event subscriptions: Ensure your endpoint is subscribed to the events you're testing.
Check firewall rules: Verify your server accepts incoming requests from Stripe's IP ranges.
Signature Verification Failing
Raw body required: Parse webhook body as raw text, not JSON, before verification:
// Correct - raw body
app.post('/webhook', express.raw({type: 'application/json'}), handler);
// Wrong - parsed JSON body
app.post('/webhook', express.json(), handler);
Correct signing secret: Use the webhook signing secret from the specific endpoint in the Stripe Dashboard, not your API key.
Header case sensitivity: Use exact header name stripe-signature, not Stripe-Signature.
Webhooks Timing Out
Return 2xx immediately: Don't wait for database operations or external API calls before responding:
// Correct
res.json({received: true});
processEventAsync(event);
// Wrong - will timeout
await processEvent(event);
res.json({received: true});
Optimize processing: Move complex logic to background jobs. Stripe retries timeouts, causing duplicate processing.
Duplicate Events
Implement idempotency: Track processed event IDs to prevent duplicate actions:
const processedEvents = new Set();
if (processedEvents.has(event.id)) {
return res.json({received: true});
}
await handleEvent(event);
processedEvents.add(event.id);
Use database constraints: Unique constraints on Stripe resource IDs prevent duplicate database records.
Conclusion
Stripe webhooks enable real-time, event-driven integrations that power modern payment systems. By implementing secure webhook endpoints, handling critical payment and subscription events, and following production best practices, you build reliable payment infrastructure that scales with your business.
Key takeaways:
- Always verify webhook signatures to ensure events come from Stripe
- Process events idempotently to handle Stripe's retry mechanism safely
- Focus on payment and subscription events for core SaaS functionality
- Test thoroughly using Stripe CLI before production deployment
- Monitor webhook deliveries to catch and resolve issues quickly
For common notification workflows like payment confirmations and subscription updates, consider workflow automation to reduce development time while maintaining reliable customer communication.
Whether you build custom webhook handlers, use automation platforms, or combine both approaches, Stripe webhooks provide the foundation for responsive, event-driven payment experiences.
