Skip to main content

Knowledge > Runbooks > Technical Ops > Debug a Failed Stripe Webhook

Debug a Failed Stripe Webhook

Diagnose and fix a Stripe webhook that failed, returned an error, or was never delivered. Stripe retries failed webhooks automatically for 3 days — fix the issue before retrying to avoid duplicate processing.

Prerequisites

  • Stripe CLI installed and logged in (stripe login)
  • Access to Vercel logs
  • The failed event ID (from Stripe Dashboard or a user report)
  • For live mode writes: STRIPE_LIVE_SECRET_KEY from knowledge/.env

Steps

  1. Find the failed webhook event in Stripe

    In Stripe Dashboard → Developers → Webhooks → select the endpoint → view recent deliveries. Or search by event ID in Stripe Dashboard → Developers → Events.

    Using CLI (test mode):

    stripe events list --limit 10

    For live mode:

    stripe events list --limit 10 --api-key $STRIPE_LIVE_SECRET_KEY
  2. Review the response code and body

    In the Stripe Dashboard event detail, check:

    • HTTP response code from your endpoint (200 = success, anything else = failure)
    • Response body (look for error messages from your handler)
    • Delivery timestamp and latency

    Common failure codes:

    • 500 — server error in the webhook handler (check Vercel logs)
    • 400 — bad request, often a signature verification failure
    • 404 — wrong webhook URL configured
    • timeout — handler took longer than 30 seconds
  3. Check Vercel logs for the webhook route

    vercel logs --tail

    Or filter by function in Vercel Dashboard → Project → Functions → /api/stripe/webhook.

    Webhook routes by codebase:

    • ChurchWiseAI: churchwiseai.com/api/stripe/webhook
    • PewSearch: pewsearch.com/api/stripe/webhook
    • ITW: illustratetheword.com/api/stripe/webhook
  4. Diagnose the root cause

    Signature verification failure (400 error):

    • The STRIPE_WEBHOOK_SECRET env var doesn't match the endpoint's signing secret
    • Verify: Stripe Dashboard → Endpoint → Signing secret → compare to Vercel env var
    • Fix: echo "whsec_..." | vercel env add STRIPE_WEBHOOK_SECRET production

    Event type not handled (silently dropped):

    • The event type isn't in the handler's switch/if block
    • Known gap: customer.subscription.trial_will_end (FA-004 pending)
    • Fix: add the event type to the handler

    Handler code bug (500 error):

    • Check Vercel logs for the stack trace
    • Common issues: null reference when subscription metadata missing, wrong table name, type mismatch

    Wrong webhook URL (404):

    • Verify the URL in Stripe matches the deployed route exactly
    • Stripe Dashboard → Developers → Webhooks → check endpoint URL
  5. Replay the event to test your fix

    After deploying the fix, replay the event:

    stripe events resend evt_1234567890abcdef

    Or replay from Stripe Dashboard → event detail → "Resend" button.

    To trigger a test event locally:

    stripe listen --forward-to localhost:3002/api/stripe/webhook
    # In a second terminal:
    stripe trigger checkout.session.completed
    stripe trigger customer.subscription.updated
    stripe trigger invoice.payment_failed
  6. Verify the event was processed correctly

    Check the database for the expected state change:

    -- Confirm subscription was updated in premium_churches
    SELECT church_id, plan, tier, stripe_subscription_id, updated_at
    FROM premium_churches
    ORDER BY updated_at DESC LIMIT 5;

    Check ops_error_reports for new errors triggered by the replay:

    SELECT * FROM ops_error_reports
    WHERE route ILIKE '%stripe%' AND created_at > now() - interval '10 minutes';

Verification

  • Stripe Dashboard shows event delivery status as 200 OK
  • Database reflects the correct subscription state
  • No new errors in ops_error_reports for the webhook route

Rollback

If a replayed event causes duplicate data (e.g., double-inserted subscription record):

  1. Identify the duplicate row in premium_churches
  2. Get founder approval before deleting
  3. Delete the duplicate using the record id, not a bulk delete

See Also