---
title: "Stripe Webhook"
method: POST
path: "/subscriptions/webhooks/stripe"
tags: ["subscriptions"]
---

# Stripe Webhook

`POST /subscriptions/webhooks/stripe`

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.

## Headers

- `Stripe-Signature` string

## Response `200`

Successful Response

- unknown

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/fluentea/apis/aurqa-language-learning-platform-refactored.md) · [All operations](https://skmtc.dev/fluentea/apis/aurqa-language-learning-platform-refactored/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/fluentea/aurqa-language-learning-platform-refactored/revisions/aea5e6e7d830/schema)
