Skip to content

Stripe Webhook Setup Guide

Overview

Stripe webhooks are essential for keeping your database in sync with payment and subscription events. When a payment succeeds, a subscription changes, or an invoice is paid, Stripe sends real-time notifications to your server.

Webhook Endpoint: /api/billing/webhooks/stripe/


Why Webhooks Are Important

Webhooks ensure that:

  • ✅ Subscriptions are activated immediately after payment
  • ✅ Failed payments update subscription status correctly
  • ✅ Plan changes are reflected in your database
  • ✅ Credits are added after successful purchases
  • ✅ Cancellations are processed automatically

Without webhooks, your database would be out of sync with Stripe's records.


Handled Webhook Events

The system handles the following Stripe events:

Payment Events

  • payment_intent.succeeded - Payment completed successfully
  • payment_intent.failed - Payment failed (card declined, etc.)

Subscription Events

  • customer.subscription.created - New subscription created
  • customer.subscription.updated - Subscription plan or status changed
  • customer.subscription.deleted - Subscription cancelled

Invoice Events

  • invoice.payment_succeeded - Invoice paid successfully, renews subscription
  • invoice.payment_failed - Invoice payment failed, marks subscription as past due
  • invoice.finalized - Invoice generated and ready to be paid

Checkout Events

  • checkout.session.completed - Checkout completed, activates subscription

Setup Instructions

Step 1: Get Your Stripe Keys

  1. Go to Stripe Dashboard
  2. Copy your Secret key (starts with sk_test_ or sk_live_)
  3. Copy your Publishable key (starts with pk_test_ or pk_live_)
  4. Add them to your .env file:
STRIPE_SECRET_KEY=sk_test_your_key_here
STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here

Step 2: Configure Webhook Endpoint

For Production:

  1. Go to Stripe Dashboard → Webhooks
  2. Click "Add endpoint"
  3. Enter your endpoint URL:
    https://yourdomain.com/api/billing/webhooks/stripe/
    
  4. Click "Select events" and choose:
  5. checkout.session.completed
  6. customer.subscription.created
  7. customer.subscription.updated
  8. customer.subscription.deleted
  9. invoice.payment_succeeded
  10. invoice.payment_failed
  11. invoice.finalized
  12. payment_intent.succeeded
  13. payment_intent.failed

  14. Click "Add endpoint"

  15. Copy the Signing secret (starts with whsec_)
  16. Add it to your .env file:
STRIPE_WEBHOOK_SECRET=whsec_your_secret_here

For Local Development:

Use the Stripe CLI to forward webhooks to your local server:

# Install Stripe CLI (macOS)
brew install stripe/stripe-cli/stripe

# Or download from: https://stripe.com/docs/stripe-cli

# Login to your Stripe account
stripe login

# Forward webhooks to local server
stripe listen --forward-to localhost:8000/api/billing/webhooks/stripe/

# Output will show your webhook signing secret
# Add it to .env as STRIPE_WEBHOOK_SECRET

The Stripe CLI will display the webhook secret. Add it to your .env:

STRIPE_WEBHOOK_SECRET=whsec_xxx_from_cli_output

Step 3: Verify Setup

Test that webhooks are working:

# With Stripe CLI running, trigger a test event
stripe trigger payment_intent.succeeded

# Check your server logs for:
# INFO: Received Stripe webhook: payment_intent.succeeded

Environment Variables

Your .env file should contain:

# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_51xxxxx...
STRIPE_PUBLISHABLE_KEY=pk_test_51xxxxx...
STRIPE_WEBHOOK_SECRET=whsec_xxxxx...

Test vs Live Mode

  • Test Mode: Use sk_test_* and pk_test_* keys during development
  • Live Mode: Use sk_live_* and pk_live_* keys in production
  • Each mode has its own webhook endpoints and signing secrets

Security

Signature Verification

All webhooks are verified using Stripe's signature verification:

  1. Stripe signs each webhook with your webhook secret
  2. Your server verifies the signature before processing
  3. Fake/tampered webhooks are rejected with 400 error

This prevents malicious actors from sending fake payment events.

CSRF Exemption

The webhook endpoint is exempt from CSRF protection because: - Stripe cannot send CSRF tokens - Signature verification provides better security - The endpoint only accepts POST requests


Webhook Processing

When a webhook is received:

1. Stripe sends POST to /api/billing/webhooks/stripe/
2. Server verifies Stripe signature
3. Server processes event based on type
4. Server updates database (subscriptions, invoices, credits)
5. Server returns 200 OK to acknowledge receipt

Important: If your server returns anything other than 200, Stripe will retry the webhook automatically.


Testing Webhooks

Test with Stripe CLI

# Make sure Stripe CLI is listening
stripe listen --forward-to localhost:8000/api/billing/webhooks/stripe/

# In another terminal, trigger test events:

# Test successful payment
stripe trigger payment_intent.succeeded

# Test subscription creation
stripe trigger customer.subscription.created

# Test invoice payment
stripe trigger invoice.payment_succeeded

# Test payment failure
stripe trigger payment_intent.failed

Test Cards (in Stripe Checkout)

Use these test card numbers in Stripe test mode:

  • Success: 4242 4242 4242 4242
  • Decline: 4000 0000 0000 0002
  • Requires 3D Secure: 4000 0025 0000 3155
  • Insufficient Funds: 4000 0000 0000 9995

Use any future expiry date and any 3-digit CVC.


Monitoring Webhooks

Stripe Dashboard

View webhook events and delivery status:

  1. Go to Stripe Dashboard → Webhooks
  2. Click on your endpoint
  3. View recent events and responses
  4. Check for failed deliveries (non-200 responses)

Application Logs

Check your server logs for webhook processing:

# Docker
docker compose logs -f web | grep webhook

# Standard
tail -f /var/log/app.log | grep webhook

Look for: - ✅ INFO: Received Stripe webhook: <event_type> - ✅ INFO: Updated subscription <id> to ACTIVE - ❌ ERROR: Error handling webhook event


Troubleshooting

Webhook Events Not Received

Problem: Stripe sends webhooks but they're not reaching your server

Solutions: 1. Verify endpoint URL is correct and publicly accessible 2. Check firewall/security group allows inbound HTTPS traffic 3. Ensure your server is running and responding 4. Check webhook logs in Stripe Dashboard for errors

Signature Verification Fails

Problem: All webhooks return 400 error

Solutions: 1. Verify STRIPE_WEBHOOK_SECRET matches Stripe Dashboard 2. Check you're using the secret for the correct endpoint 3. Ensure you're not modifying the request body before verification 4. For local development, use the secret from stripe listen output

Webhooks Received but Not Processing

Problem: Webhooks return 200 but database not updating

Solutions: 1. Check server logs for errors during processing 2. Verify database connection is working 3. Check that Stripe IDs match (customer_id, subscription_id) 4. Ensure subscription/customer records exist in database

Duplicate Events

Problem: Same event processed multiple times

Note: This is normal behavior. Stripe may send the same event multiple times for reliability. Your webhook handlers are designed to be idempotent (safe to process multiple times).


Webhook Retry Logic

Stripe automatically retries failed webhooks:

  • Initial attempt: Immediate
  • Retry 1: After 5 minutes
  • Retry 2: After 30 minutes
  • Retry 3: After 2 hours
  • Retry 4: After 4 hours
  • Retry 5: After 8 hours

After all retries fail, the event is marked as failed in the dashboard.

Best Practice: Fix issues quickly so retries succeed.


What Happens During Each Event

checkout.session.completed

  • Finds subscription by customer ID
  • Links Stripe subscription ID
  • Activates subscription
  • Allocates monthly credits

customer.subscription.updated

  • Updates subscription status (active, past_due, cancelled)
  • Updates billing period dates
  • Reflects plan changes

invoice.payment_succeeded

  • Marks invoice as paid
  • Renews subscription credits
  • Extends subscription period

invoice.payment_failed

  • Marks invoice as uncollectible
  • Sets subscription to past_due
  • (TODO: Send email notification)

payment_intent.succeeded

  • Updates transaction status
  • For credit purchases: Adds credits to workspace

payment_intent.failed

  • Updates transaction status to failed
  • Records error message

Development Workflow

Standard Development Flow

# Terminal 1: Start your Django server
docker compose up

# Terminal 2: Forward Stripe webhooks
stripe listen --forward-to localhost:8000/api/billing/webhooks/stripe/

# Terminal 3: Trigger test events
stripe trigger checkout.session.completed
stripe trigger invoice.payment_succeeded

Testing a Complete Subscription Flow

  1. Create a test subscription via Checkout
  2. Use test card 4242 4242 4242 4242
  3. Complete payment
  4. Watch webhook logs for:
  5. checkout.session.completed
  6. customer.subscription.created
  7. invoice.payment_succeeded
  8. Verify subscription is active in database

Production Checklist

Before going live:

  • [ ] Switch to live Stripe keys (sk_live_*, pk_live_*)
  • [ ] Create production webhook endpoint in Stripe Dashboard
  • [ ] Add production webhook secret to .env
  • [ ] Test webhook endpoint is publicly accessible
  • [ ] Monitor webhook delivery in Stripe Dashboard
  • [ ] Set up alerts for failed webhooks
  • [ ] Configure Customer Portal in Stripe Dashboard
  • [ ] Test end-to-end subscription flow with live mode test

External Resources