Skip to content

[Bug] Stripe webhook returns HTTP 200 on processing failure, preventing Stripe retries #3000

@rohansx

Description

@rohansx

Bug

The Stripe webhook handler in backend/core/billing/external/stripe/webhooks.py returns HTTP 200 with {'status': 'success'} even when webhook processing fails with an exception (line 107).

# Line 91-107
except Exception as e:
    logger.error(f"[WEBHOOK] Error processing webhook: {e}")
    # ...error logging...
    if event and hasattr(event, 'id'):
        await WebhookLock.mark_webhook_failed(event.id, error_message)

    return {'status': 'success', 'error': 'processed_with_errors', 'message': 'Webhook logged as failed internally'}

The endpoint in backend/core/billing/endpoints/webhooks.py directly returns this dict, so FastAPI sends a 200 response to Stripe.

Impact

  • Stripe stops retrying failed webhooks — Stripe interprets any 2xx response as successful delivery and will not attempt redelivery (Stripe docs: retry behavior)
  • Billing events are silently lost — failed checkout.session.completed, customer.subscription.updated, invoice.payment_succeeded, etc. events are never retried
  • Users may pay but not receive their subscription — if the CheckoutHandler or SubscriptionHandler throws an exception, the payment went through on Stripe's side but Suna never processes it
  • The failure IS logged internally and marked via WebhookLock.mark_webhook_failed(), but there is no automatic recovery path since Stripe won't retry

Expected behavior

When webhook processing fails, the handler should return a non-2xx status code (e.g., 500) so that Stripe retries the event delivery using its exponential backoff schedule (up to 3 days). The handler should still log the failure internally, but the HTTP response must signal failure to Stripe.

Suggested fix

Replace the return on line 107 with an HTTP 500 response:

except Exception as e:
    logger.error(f"[WEBHOOK] Error processing webhook: {e}")
    # ...existing error logging...
    if event and hasattr(event, 'id'):
        await WebhookLock.mark_webhook_failed(event.id, error_message)

    raise HTTPException(status_code=500, detail="Webhook processing failed")

This ensures Stripe retries the event while still preserving the internal failure tracking via WebhookLock.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions