Skip to main content
The webhook endpoint handles three Stripe events that cover the full subscription billing lifecycle. Each event is dispatched inside the switch block in app/routes/webhook.ts.
Stripe guarantees at-least-once delivery, meaning the same event may arrive more than once. Make your handlers idempotent — checking whether the action has already been applied before writing to your database.
The Stripe Dashboard event log (Developers → Events) shows every event Stripe has sent, its payload, and the delivery status. Use it to replay failed events and debug handler logic without triggering real billing actions.

Handled events

When it fires: Stripe successfully collects payment for an invoice. This occurs at the start of each billing period for active subscriptions, or immediately after a customer completes checkout.Triggered by: Automatic billing cycle or a customer completing payment.What to do: Provision or renew the customer’s access to your product. After this event, the subscription status is active.
case "invoice.paid": {
  // Grant access, update subscription status to active in your database
  const invoice = event.data.object;
  const subscriptionId = invoice.subscription;
  // await db.subscriptions.update({ id: subscriptionId, status: "active" });
  break;
}
Always confirm access using invoice.paid rather than relying solely on the checkout session completion event. invoice.paid fires on every renewal, keeping long-lived subscriptions in sync.
When it fires: A payment attempt fails because the card is declined, expired, or no valid payment method is on file. Stripe will retry according to your retry schedule.Triggered by: Automatic billing cycle — not a direct user action.What to do: Notify the customer that their payment failed and prompt them to update their payment method. After this event, the subscription status becomes past_due.
case "invoice.payment_failed": {
  const invoice = event.data.object;
  const customerId = invoice.customer;
  // await sendPaymentFailedEmail(customerId);
  // await db.subscriptions.update({ customerId, status: "past_due" });
  break;
}
Do not immediately revoke access on the first failure. Stripe will retry payment and send additional invoice.payment_failed events. Revoke access only after all retries are exhausted and the subscription moves to canceled.
When it fires: A subscription is cancelled — either at the end of the billing period (scheduled cancellation) or immediately.Triggered by: A user-initiated cancellation request or automatic cancellation by Stripe after all payment retries fail. Use event.request to tell them apart:
  • event.request !== null — cancelled by an API request (user or your app)
  • event.request === null — cancelled automatically by Stripe
What to do: Revoke access to your product for that customer and update their record to reflect the cancelled status.
case "customer.subscription.deleted": {
  const subscription = event.data.object;
  const isManualCancellation = event.request !== null;
  // await db.subscriptions.update({
  //   id: subscription.id,
  //   status: "canceled",
  //   canceledByUser: isManualCancellation,
  // });
  break;
}

Event summary

EventSubscription status afterTriggered byAction
invoice.paidactiveAutomatic / checkoutProvision access
invoice.payment_failedpast_dueAutomaticNotify customer
customer.subscription.deletedcanceledUser request or automaticRevoke access

Build docs developers (and LLMs) love