---
title: "Stripe Webhook"
method: POST
path: "/billing/stripe-webhook"
tags: ["billing"]
---

# Stripe Webhook

`POST /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.

## Headers

- `stripe-signature` string, nullable

## Response `200`

Successful Response

- object

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-24** `1cffaad2a921` — 1 warning, 1 info
  - removed the optional property `detail` from the response with the `422` status
  - added the required property `error` to the response with the `422` status

[Change history](https://skmtc.dev/pioneer/apis/brain-api/changes/billing/stripe-webhook/post.md)

---

[API](https://skmtc.dev/pioneer/apis/brain-api.md) · [All operations](https://skmtc.dev/pioneer/apis/brain-api/llms.txt) · [OpenAPI document](https://skmtc.dev/pioneer/apis/brain-api/revisions/1cffaad2a921?raw)
