Docs · Billing & Entitlements

Stripe Setup

Getting your secret key, webhook secret, and price IDs, and setting a statement descriptor.

Turning on real billing takes three environment variables and one setting inside Stripe itself. Nothing here is needed to develop against the app: the Local fake covers that. This is the checklist for the point where you're ready to charge a real card.

The three variables

  • STRIPE_SECRET_KEY: start with a test key (sk_test_...) while you're integrating, and swap to a live key only when you're ready to charge real cards.
  • STRIPE_WEBHOOK_SECRET: from your webhook endpoint at /webhooks/stripe. Locally, run stripe listen --forward-to localhost:3000/webhooks/stripe and use the secret it prints.
  • A price id per plan, for example STRIPE_PRICE_PRO: the id of a recurring Price you create in Stripe, either directly in the dashboard or through the Stripe MCP server (see .mcp.json).

Secrets resolve ENV first, then Rails encrypted credentials (bin/rails credentials:edit) as a fallback. Locally, put them in .env (copy .env.example as a starting point); in production, inject them via Kamal secrets. See First Deploy Checklist.

Set your statement descriptor before you charge anyone

By default, a customer's card statement shows your Stripe account's registered business name, not your product's name. Someone who bought from yourapp.com sees an unfamiliar legal entity on their statement, doesn't recognize it, and disputes the charge. Unrecognized descriptors are a leading cause of chargebacks, and the risk compounds if one Stripe account serves more than one product.

For subscriptions, the descriptor comes from the Stripe Product (max 22 characters, shown in capitals), with precedence running invoice, then product, then account default. Set it on each product:

Stripe::Product.update("prod_...", statement_descriptor: "YOURAPP.COM")

Your domain is usually the clearest choice: it's what the customer remembers paying. A product created inline from price_data cannot carry a descriptor at all, which is the reason to create real Products and Prices in Stripe and reference their ids from environment variables, rather than building line items inline at checkout time.

Webhook events to enable

Your Stripe webhook endpoint needs to receive subscription lifecycle events: created, updated, canceled, and payment failures, at minimum. Missing one of these means the app's local billing state can drift from what Stripe actually has on file, since entitled? reads that local state rather than calling Stripe on every request. See First Deploy Checklist for the full list this kit's own deploy expects.

Verify

With test-mode keys in place, walk through a full subscribe-and-cancel cycle on the pricing page and confirm the webhook fires and updates entitlement correctly, before pointing the same setup at live keys.

Next

If you want to run a limited-time discount instead of (or alongside) your standard pricing, see Launch Pricing.

Common 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.

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.

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.