billing

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.

post/billing/stripe-webhook

Headers

stripe-signaturestring nullable

Response

Successful Response

object required

Changes