subscriptions

Stripe Webhook

Handle Stripe webhook events.

TRANSACTION STRUCTURE (fixed 2026-09-01 - see play_billing_routes.py's _record_event() for the reference pattern this now matches):

Every branch below uses db.flush(), never db.commit(). The ONLY db.commit() in this function is the single one at the very end, right after the idempotency marker (WebhookEvent row) is added - so a webhook's business effects (crediting tokens, updating a subscription's tier, granting a signup bonus, ...) and the fact that we consider it "processed" land in one all-or-nothing transaction. If anything raises partway through, get_db()'s dependency rolls the whole session back and this function's own except-block returns 500, so:

  • NOTHING partial was persisted (flush is visible within the transaction for subsequent queries, but never durable until commit), and
  • the event was NOT marked processed, so Stripe's automatic retry will reprocess it cleanly - not double-apply it. Before this fix, several branches called db.commit() directly, so a crash (or a swallowed exception - checkout.session.completed used to catch-and-log-only) between an early commit and the final marker write could durably apply a business effect (e.g. credit purchased tokens) while never recording the event as processed, so Stripe's retry would apply it AGAIN. See the regression tests in backend/tests/unit/routes/test_stripe_webhook_idempotency.py.

Two shared services this handler calls also commit internally by default (TokenEconomyService.add_bonus_tokens / get_or_create_token_account, used elsewhere with their normal commit-immediately behavior) - both now accept commit=False and this handler passes it, so the signup-bonus grant participates in the same one-transaction-per-event guarantee. cost_tracking_service.track_trial_start/track_new_subscription (called from the customer.subscription.created branch) still commit internally and swallow their own errors; that is a pre-existing, separate gap in a shared analytics-metrics helper (not customer-facing money/entitlements - worst case on a retry is a daily counter double-incrementing by one) and was judged out of scope for this fix. Flagged here rather than silently left unmentioned.

post/subscriptions/webhooks/stripe

Headers

Stripe-Signaturestring

Response

Successful Response

{"stackTrail":"paths:/subscriptions/webhooks/stripe:post:responses:200:content:application/json:schema","oasType":"schema","type":"unknown"}

Changes