# Stripe checkout.session.expired: rebuild the session, do not retry it

## The symptom

Customers report an error on a Stripe-hosted checkout page. Your logs show `checkout.session.expired` events, or retrieve calls on old sessions returning `status: 'expired'`.

## Confirm the cause

A session expires at `expires_at`, default 24 hours after creation. Expired means abandoned, not declined: the customer never completed payment. Check `session.expires_at` vs now. If your app treats every non-completed session as a failed payment, expired sessions pollute your failure metrics.

## The fix

An expired session is dead. You cannot reopen it; create a new one:

```js
if (event.type === 'checkout.session.expired') {
  const session = event.data.object;
  await db.orders.markAbandoned(session.client_reference_id, { reason: 'expired' });
  await sendRecoveryEmail(session.customer_email, buildNewCheckoutLink(session));
}
```

`expires_at` rules:
- You can set a custom expiry between 30 minutes and 24 hours. Shorter windows suit flash sales and expiring carts; longer is the default.
- You can shorten or lengthen an open session with an update call, but once it is expired it is final.

Two pipeline details:
1. The `expired` event is not enabled on every webhook endpoint by default. Add `checkout.session.expired` in the dashboard webhook settings or you will never see it.
2. Log expired and failed payments separately. Expiry is a marketing/recovery event (send a fresh link). Failure is a payments event (fix the payment method). Mixing them sends the wrong email.

## Verify the fix

Create a session with `expires_at` 30 minutes out, wait it out (or simulate via the event), and confirm your handler marks the order abandoned and the recovery email contains a fresh working session link. Confirm the old session id returns `expired` and cannot be reused.
