> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe Webhook Events

> The five Stripe events AspFox handles, what triggers each, and what local state changes result.

AspFox handles five Stripe webhook events. The endpoint is `POST /api/v1/webhooks/stripe`.

## Handled events

| Event                           | What triggers it in Stripe                                           | AspFox action                                                                                                           | Local state change                                                                                                                                      |
| ------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkout.session.completed`    | Customer completes payment on Stripe's hosted checkout page          | Creates or updates the local `Subscription` record. Reads `tenantId` from `session.metadata`.                           | Sets `Plan` and `Status` to `Trialing` (if trial started) or `Active`. Sets `StripeSubscriptionId` and `StripeCustomerId`.                              |
| `customer.subscription.updated` | Plan change, trial conversion to paid, cancellation scheduled, pause | Updates local subscription fields to match Stripe.                                                                      | Updates `Plan`, `Status`, `CurrentPeriodEnd`, `CancelAtPeriodEnd`. If `cancel_at_period_end` becomes true, creates `SubscriptionCanceled` notification. |
| `customer.subscription.deleted` | Subscription fully canceled (immediately or at period end)           | Marks local subscription as canceled.                                                                                   | Sets `Status = Canceled`.                                                                                                                               |
| `invoice.payment_failed`        | Stripe could not charge the customer's payment method                | Marks local subscription as past due. Sends payment failed email. Creates `SubscriptionPastDue` notification for owner. | Sets `Status = PastDue`.                                                                                                                                |
| `invoice.payment_succeeded`     | Stripe successfully charged the customer                             | If subscription was `PastDue`, recovers it. Sends payment recovered email. Creates `PaymentRecovered` notification.     | If was `PastDue`: sets `Status = Active`, sends email and notification. If already `Active`: no-op.                                                     |

## Idempotency

Every handler checks current state before writing. Running the same handler twice with the same event produces the same result:

* `checkout.session.completed` — checks if `StripeSubscriptionId` already matches before creating a new record
* `customer.subscription.updated` — compares incoming values to current values; if identical, no database write
* `customer.subscription.deleted` — if status is already `Canceled`, no-op
* `invoice.payment_failed` — if status is already `PastDue`, skips the email and notification to avoid duplicate sends
* `invoice.payment_succeeded` — if status is already `Active`, no-op

<Note>
  AspFox returns `200 OK` for **all** webhook events, including events it does not handle. Stripe requires a 2xx response to consider delivery successful. Returning non-2xx for unhandled events causes Stripe to retry indefinitely and eventually disable the endpoint.
</Note>

## Webhook signature verification

Every request to the webhook endpoint is verified using the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`. Requests with an invalid or missing signature are rejected with `400 Bad Request` before any processing occurs.

Do not disable signature verification. The webhook endpoint is public — without signature verification, anyone could send fake webhook events to manipulate subscription state.

## Webhook delivery timing

Stripe delivers webhooks asynchronously after the triggering event. The delay is typically under 30 seconds but can be longer during Stripe incidents. AspFox does not poll Stripe on every request — local state may lag behind Stripe by up to 30 seconds after an event, or up to 2 hours in the case of a missed webhook (corrected by `SubscriptionSyncJob`).
