Stripe Webhook
Handle Stripe webhook events.
Verifies the Stripe signature, deduplicates by event id, and routes the event to its billing-lifecycle handler. Unprotected (no auth) but signature-verified.
Events handled: checkout.session.completed, invoice.paid, invoice_payment.paid, invoice.payment_failed, payment_method.attached / .detached, charge.refunded, charge.dispute.created / .closed, refund.updated / .created, customer.deleted, radar.early_fraud_warning.created.
invoice.paid and invoice_payment.paid share _handle_invoice_paid: Stripe emits both for a single payment on the pinned API version. They carry different event ids, so the ledger's event-id key does not dedupe one against the other -- record_refill_topup instead nominates invoice.paid as the only delivery that writes money, and the sibling returns having written nothing. The event-id key is what makes a redelivery of the same event safe.
invoice.payment_failed carries the terminal dunning signal as well as the retryable ones, and is the only route that suspends for non-payment. Stripe emits no explicit "collection is over" event -- reaching uncollectible always takes an operator or an Automation, neither of which exists here -- so exhaustion is read as a failed attempt carrying no next_payment_attempt on an invoice still open. If Billing Automations are ever enabled that field moves to invoice.updated and this design must be revisited; that caveat is a guardrail, not a reason to read the noisier event today. See services.billing.retry_exhaustion.
Args: request: Inbound FastAPI request carrying the raw Stripe payload. stripe_signature: Value of the stripe-signature header.
Returns: {"status": "success"} once the event is processed.
Raises: HTTPException: 500 when the webhook secret is unconfigured or processing fails; 400 for an invalid signature.
Headers
Response
Successful Response