Stripe webhook event log showing delivery attempts, response codes and retry status in a developer dashboard
# developer tools# website monitoring

How to Monitor Stripe Webhooks and Checkout Flows

Stripe is reliable. Your webhook endpoint is not always reliable. This is the gap that causes silent billing failures — subscriptions that should activate don't, cancellations that aren't processed, payments that succeed in Stripe but never trigger fulfillment in your application.

Monitoring your Stripe integration means monitoring both sides: Stripe's delivery of events and your endpoint's handling of them.

Why Webhooks Fail Silently

When Stripe delivers a webhook to your endpoint, it expects a 2xx response within 30 seconds. If your endpoint returns anything else — a 500, a timeout, a redirect — Stripe marks the delivery as failed and retries. After multiple failed attempts, the event is marked as undeliverable.

The problem: none of this generates a visible error in your application. Users don't see a failure message. You don't get an email. Your dashboard looks normal. Meanwhile, subscription activations, payment confirmations, and cancellation cleanups are silently not happening.

The Stripe Webhook Monitoring Baseline

Step 1: Enable Stripe's webhook event log. In your Stripe dashboard, navigate to Developers → Webhooks → your endpoint. Stripe shows the delivery attempt history, response codes, and retry status for every event. Check this regularly after deployments, as a broken deployment is the most common cause of sudden webhook failures.

Step 2: Monitor your webhook endpoint externally. Your webhook endpoint URL should return a fast, correct response. Add it as an uptime monitor — not for HTTP GET monitoring (webhooks are POST), but to confirm the route exists and your application is serving it. A 405 Method Not Allowed response from a GET request to /webhook/stripe confirms the endpoint is live; a 404 means something has changed in your routing.

Step 3: Alert on Stripe webhook failures. Stripe doesn't natively send alerts when events fail. Use Stripe's API to query for failed events:

import stripe

# Check for events that failed delivery in the last hour
events = stripe.WebhookEndpoint.list()
for endpoint in events.data:
    # Check endpoint delivery success rate
    pass

# Or directly check events
failed = stripe.Event.list(
    delivery_success=False,
    created={'gte': int(time.time()) - 3600}
)

if failed.data:
    # Alert — events failed delivery in the last hour
    alert_team(f"{len(failed.data)} webhook events failed delivery")

Schedule this check to run every 15–30 minutes.

Monitoring the Checkout Flow

Webhook monitoring catches processing failures. Checkout monitoring catches the user-facing flow itself. These are different things — your webhooks can be working while your checkout page is broken.

Add a dedicated monitor for your checkout page URL. A checkout that returns a 500, redirects incorrectly, or fails to load Stripe.js is invisible to your webhook monitoring.

For the most critical path monitoring — actually testing that a checkout can be initiated — you can use Stripe's test mode to create synthetic transactions:

// A smoke test endpoint that confirms Stripe is configured correctly
app.get('/health/stripe', async (req, res) => {
    try {
        // Verify Stripe key works by listing one product
        const products = await stripe.products.list({ limit: 1 });
        res.json({ status: 'ok', stripe: 'reachable' });
    } catch (err) {
        res.status(503).json({ status: 'error', stripe: err.message });
    }
});

Point a monitor at /health/stripe to confirm your Stripe integration is configured and reachable, even without processing a real transaction.

Idempotency and Replay Safety

Stripe retries failed webhooks. Your endpoint must be idempotent — processing the same event twice must not cause double fulfillment, double provisioning, or duplicate charges.

Always check whether an event has already been processed before acting on it:

def handle_payment_succeeded(event):
    payment_intent_id = event['data']['object']['id']

    # Check if already processed
    if Order.objects.filter(stripe_payment_intent=payment_intent_id).exists():
        return  # Already handled

    # Process the payment
    create_order(event['data']['object'])

This is especially important once you have monitoring and alerting that might trigger manual replays.

Connecting to Uptime Monitoring

Domain Monitor monitors your application URLs every minute from multiple locations. Add monitors for:

  1. Your checkout page
  2. Your webhook endpoint (GET request to confirm routing is live)
  3. Your Stripe health check endpoint

Create a free account and set alerts to fire immediately if any of these return unexpected responses. See how to set up downtime alerts for alert configuration.

Also in This Series

More posts

Why Your Status Page Matters During an Outage

When your site goes down, your status page becomes the most important page you have. Here's why it matters, what happens when you don't have one, and what a good status page does during a real outage.

Read more
Why Your Domain Points to the Wrong Server

Your domain is resolving, but pointing to the wrong server — showing old content, a previous host's page, or someone else's site entirely. Here's what causes this and how to diagnose it.

Read more
Why Website Monitoring Misses Downtime Sometimes

Uptime monitoring isn't foolproof. Single-location monitors, wrong health check endpoints, long check intervals, and false positives can all cause real downtime to go undetected. Here's what to watch out for.

Read more

Subscribe to our PRO plan.

Looking to monitor your website and domains? Join our platform and start today.