Answers · Billing

Which Stripe webhook events does a subscription app need?

Eight: checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, charge.refunded, charge.dispute.created and radar.early_fraud_warning.created. The first five keep entitlement correct; the last three are the money events you want to know about rather than discover in a statement.

What each one is for

Event What it changes
checkout.session.completed Activates the subscription after a successful checkout.
invoice.payment_succeeded Renewal succeeded. Keeps an active row active.
invoice.payment_failed Moves the account to past due rather than cutting access instantly.
customer.subscription.updated Plan change, trial ending, status change from Stripe's side.
customer.subscription.deleted Cancellation took effect. Access ends.
charge.refunded You refunded someone. Worth reacting to rather than finding later.
charge.dispute.created A chargeback. Has a deadline attached.
radar.early_fraud_warning.created Stripe thinks a charge is fraudulent before the dispute.

The handler for all of them is one controller, and the route is declared in config/routes.rb under the webhooks namespace.

Enable exactly these, not everything

Stripe's dashboard offers a select-all. Taking it sends hundreds of event types, most of which your endpoint ignores. That is not harmless: a busy endpoint that returns 2xx to noise makes the delivery log useless for debugging the five events you care about, and it costs you the signal when one of them starts failing.

Verify the endpoint is actually receiving

bin/rails runner 'puts Rails.application.routes.url_helpers.webhooks_stripe_path'

Then check Stripe's own delivery log in the dashboard. It shows the response code for every attempt, which is the fastest way to tell a signature problem from a routing problem.

Signature verification is not optional

The endpoint is unauthenticated by necessity, so the signature is the only thing distinguishing Stripe from anyone who found the URL. STRIPE_WEBHOOK_SECRET is what verifies it, and a mismatched secret returns 400 for every delivery.

Entitlement is local, and that is the point

The webhook updates a Subscription row. Request-time entitlement checks read that row and never call Stripe, so a delayed webhook delays activation by seconds, and a Stripe outage does not lock out customers who already paid.

Related questions

Do I have to handle failed payments and refunds myself?

No. Stripe retries failed charges, emails customers, and processes refunds through its dashboard. Your app subscribes to invoice.payment_failed, invoice.payment_succeeded, charge.refunded and customer.subscription.deleted, and keeps one local Subscription row in sync. That is the whole of your responsibility.

What is a statement descriptor and why set it before charging?

It is the text that appears on a customer's card statement next to the charge. Set it in the Stripe dashboard before you take any money. Changing it later only affects future charges, so early customers keep whatever was there, and an unrecognizable descriptor is one of the most common causes of a chargeback.

Why does my Stripe webhook keep returning a 400 error?

Signature verification failed. Either STRIPE_WEBHOOK_SECRET does not match the endpoint sending the event, or something read and re-encoded the request body before verification ran. Each endpoint in Stripe has its own secret, and the CLI's local secret is different again, which is the usual cause of a 400 that appears only in production.