Sign in
An image of the Stripe logo
Create account
Sign in
Home
Payments
Business operations
Financial services
Developer tools
Security
All products
Home
Payments
Business operations
Home
Payments
Business operations
Financial services
Developer tools
Support
Overview
Overview
How subscriptions work
How to model subscriptions
Get started
Quickstart
Design an integration
Build a subscriptions integration
Integrate the customer portal
Migrate subscriptions to Stripe
Billing resources
Coupons
Customers
Subscriptions
Subscription invoices
Manage subscriptions
Change subscriptions
Usage-based billing
Use trial periods
Set payment methods
Subscriptions with multiple products
Set subscription quantities
Subscription webhooks
Schedule subscriptions
Tax
Integrate with Salesforce
Manage recurring revenue
Revenue recognition
Revenue recovery
Subscription metrics
Testing
Test your integration
Test clocks
Metered billing with Elements
Fixed-price billing with Elements
Per-seat billing with Elements
Migrate to Prices
Strong Customer Authentication (SCA)
Invoices API updates
Improved tax support
Testing
No-code options
Billing
·
HomePaymentsSubscriptions

Create fixed-price subscriptions

Learn how to offer multiple pricing options to your customers and charge them a fixed amount each month.

Stripe recommends creating subscriptions with payment_behavior set to default_incomplete to simplify handling failed payments. This creates subscriptions with status incomplete which allows you to collect and confirm payment information in a single user interface. See the updated guide for attempting payment after creating the subscription.

This guide walks you through how to create fixed-price subscriptions for a photo hosting service. It shows you how to use Stripe Elements to create a custom payment form you embed in your application. You can find code for an example implementation with Elements on GitHub. If your business model doesn’t have fixed prices, try metered billing or per-seat subscriptions.

You can also use Checkout if you don’t want to build a custom payment form, or one of our quickstart options if you aren’t ready to build an integration yet.

What you’ll build

This guide shows you how to:

  • Model your subscriptions with Products and Prices
  • Create a signup flow
  • Collect payment information and create the subscription
  • Test and monitor payment and subscription status
  • Handle payment errors
  • Let customers change their plan or cancel the subscription

Example application

How to model it on Stripe

API object relationships

API object definitions

Install Stripe libraries and tools

Install the Stripe client of your choice:

Terminal
# Available as a gem sudo gem install stripe
Gemfile
# If you use bundler, you can add this line to your Gemfile gem 'stripe'

And install the Stripe CLI. The CLI provides the webhook testing you’ll need, and you can run it to create your products and prices.

  1. From the command-line, use an install script or download and extract a versioned archive file for your operating system to install the CLI.

To install the Stripe CLI with homebrew, run:

Terminal
brew install stripe/stripe-cli/stripe

To run the Stripe CLI, you must also pair it with your Stripe account. Run stripe login and follow the prompts. For more information, see the Stripe CLI documentation page.

Test as you go
Server

You can test different payment scenarios with Stripe’s test cards, but testing also includes checking events to monitor the status of subscriptions. If you’re building a subscription integration for the first time, monitoring events in the Dashboard might be enough to help monitor your work while you follow the steps.

A production-ready integration should monitor events automatically, though. You can call the Stripe API and check specific values in the response (polling), or you can set up a webhook endpoint and let Stripe push events to your integration. Webhook monitoring is simpler and more efficient, especially for asynchronous integrations like subscriptions. The Stripe CLI provides a listen command for testing event monitoring during development.

Set up webhook monitoring

You can set up a webhook endpoint in the Dashboard, or with the Webhook Endpoints API.

Here’s how to set up your webhook handler and verify the signature of the event. Note that the example specifies the major event types to monitor for subscriptions, but you can add more as needed.

server.rb
# Set your secret key. Remember to switch to your live secret key in production. # See your keys here: https://dashboard.stripe.com/apikeys Stripe.api_key =
'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
post '/webhook' do # You can use webhooks to receive information about asynchronous payment events. # For more about our webhook events check out https://stripe.com/docs/webhooks. webhook_secret = ENV['STRIPE_WEBHOOK_SECRET'] payload = request.body.read if !webhook_secret.empty?

Event types to monitor are indicated at the steps in this guide where they’re applicable. For more events you can monitor, see Subscription events.

Create the business model
Stripe CLI or Dashboard

Create your products and their prices in the Dashboard or with the Stripe CLI.

This example uses a fixed-price service with two different service-level options: Basic and Premium. For each service-level option, you need to create a product and a recurring price.

If you want to add a one-time charge for something like a setup fee, create a third product with a one-time price. To keep things simple, this example doesn’t include a one-time charge.

In this example, each product bills at monthly intervals. The price for the Basic product is 5 USD; the price for the Premium product is 15 USD.

Navigate to the Add a product page and create two products. Add one price for each product, each with a monthly recurring billing period:

  • Premium product: Premium service with extra features

    • Price: Standard model | 15 USD
  • Basic product: Basic service with minimum features

    • Price: Standard model | 5 USD

You don’t need to specify the unit amount in the Dashboard unless you use the package pricing model.

After you create the prices, record the price IDs so you can use them in subsequent steps. Price IDs look like this: price_G0FvDp6vZvdwRZ.

When you’re ready, use the Copy to live mode button at the top right of the page to clone your product from test mode to live mode.

Create the Stripe customer
Client and Server

A Stripe customer object allows you to perform recurring charges for the same customer.

In this guide, you create a customer from the provided email.

On your application frontend, pass the customer email to a backend endpoint.

script.js
function createCustomer() { let billingEmail = document.querySelector('#email').value; return fetch('/create-customer', { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: billingEmail, }), }) .then((response) => { return response.json(); }) .then((result) => { // result.customer.id is used to map back to the customer object return result; }); }

You should receive a customer.created event.

Collect payment information
Client

Let your customer choose a plan and provide payment information. In this guide, the customer chooses between Basic and Premium.

Then use Stripe Elements to collect card information, and customize Elements to match the look-and-feel of the application.

Selecting a plan and collecting payment details

Set up Stripe Elements

Stripe Elements is included with Stripe.js. Include the Stripe.js script on your checkout page by adding it to the head of your HTML file.

Always load Stripe.js directly from js.stripe.com to remain PCI compliant. Don’t include the script in a bundle or host a copy of it yourself.

subscribe.html
<head> <title>Subscription prices</title> <script src="https://js.stripe.com/v3/"></script> </head>

Create an instance of Elements with the following JavaScript:

script.js
// Set your publishable key: remember to change this to your live publishable key in production // See your keys here: https://dashboard.stripe.com/apikeys let stripe = Stripe(
'pk_test_TYooMQauvdEDq54NiTphI7jx'
); let elements = stripe.elements();

Add Elements to your page

Elements needs a place to live in your payment form. Create empty DOM nodes (containers) with unique IDs in your payment form and then pass those IDs to Elements.

subscribe.html
<body> <form id="payment-form"> <div id="card-element"> <!-- Elements will create input elements here --> </div> <!-- We'll put the error messages in this element --> <div id="card-element-errors" role="alert"></div> <button type="submit">Subscribe</button> </form> </body>

Create an instance of an Element and mount it to the Element container:

script.js
let card = elements.create('card', { style: style }); card.mount('#card-element');

The card Element simplifies the form and minimizes the number of fields required by inserting a single, flexible input field that securely collects all necessary card details. For a full list of supported Element types, refer to our Stripe.js reference documentation.

Use the test card number 4242 4242 4242 4242, any three-digit CVC number, any expiration date in the future, and any five-digit ZIP code.

Elements validates user input as it is typed. To help your customers catch mistakes, listen to change events on the card Element and display any errors.

script.js
card.on('change', function (event) { displayError(event); }); function displayError(event) { changeLoadingStatePrices(false); let displayError = document.getElementById('card-element-errors'); if (event.error) { displayError.textContent = event.error.message; } else { displayError.textContent = ''; } }

ZIP code validation depends on your customer’s billing country. Use our international test cards to experiment with other postal code formats.

Save payment details and create the subscription
Client and Server

On the frontend, save the payment details you just collected to a payment method, passing the ID of the customer you already created:

script.js
var form = document.getElementById('payment-form'); form.addEventListener('submit', function (ev) { ev.preventDefault(); }); function createPaymentMethod({ card }) { const customerId = {{CUSTOMER_ID}}; // Set up payment method for recurring usage let billingName = document.querySelector('#name').value; let priceId = document.getElementById('priceId').innerHTML.toUpperCase(); stripe .createPaymentMethod({ type: 'card', card: card, billing_details: { name: billingName, }, }) .then((result) => { if (result.error) { displayError(result); } else { createSubscription({ customerId: customerId, paymentMethodId: result.paymentMethod.id, priceId: priceId, }); } }); }

Define the createSubscription function you just called, passing the customer, payment method, and price IDs to a backend endpoint.

This function calls other functions that are defined and explained in the following sections of this guide.

script.js
function createSubscription({ customerId, paymentMethodId, priceId }) { return ( fetch('/create-subscription', { method: 'post', headers: { 'Content-type': 'application/json', }, body: JSON.stringify({ customerId: customerId, paymentMethodId: paymentMethodId, priceId: priceId, }), }) .then((response) => { return response.json(); }) // If the card is declined, display an error to the user. .then((result) => { if (result.error) { // The card had an error when trying to attach it to a customer. throw result; } return result; }) // Normalize the result to contain the object returned by Stripe. // Add the additional details we need. .then((result) => { return { paymentMethodId: paymentMethodId, priceId: priceId, subscription: result, }; }) // Some payment methods require a customer to be on session // to complete the payment process. Check the status of the // payment intent to handle these actions. .then(handlePaymentThatRequiresCustomerAction) // If attaching this card to a Customer object succeeds, // but attempts to charge the customer fail, you // get a requires_payment_method error. .then(handleRequiresPaymentMethod) // No more actions required. Provision your service for the user. .then(onSubscriptionComplete) .catch((error) => { // An error has happened. Display the failure to the user here. // We utilize the HTML element we created. showCardError(error); }) ); }

On the backend, define the endpoint that creates the subscription for the frontend to call. The code updates the customer with the payment method, and then passes the customer ID to the subscription. The payment method is also assigned as the default payment method for the subscription invoices.

server.rb
# Set your secret key. Remember to switch to your live secret key in production. # See your keys here: https://dashboard.stripe.com/apikeys Stripe.api_key =
'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
post '/create-subscription' do content_type 'application/json' data = JSON.parse request.body.read begin Stripe::PaymentMethod.attach( data['paymentMethodId'], { customer: data['customerId'] } ) rescue Stripe::CardError => e halt 200, { 'Content-Type' => 'application/json' }, { 'error': { message: e.error.message } }.to_json end # Set the default payment method on the customer Stripe::Customer.update( data['customerId'], invoice_settings: { default_payment_method: data['paymentMethodId'] } ) # Create the subscription subscription = Stripe::Subscription.create( customer: data['customerId'], items: [{ price: 'price_HGd7M3DV3IMXkC' }], expand: %w[latest_invoice.payment_intent] ) subscription.to_json end

Here’s an example response. The minimum fields to store are highlighted. Store fields your application frequently accesses.

{ "id": "sub_HAwfLuEoLetEJ3", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1588008574, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically",

The example verifies initial payment status with the API response by checking the value of subscription.latest_invoice.payment_intent.status. The invoice tracks payment status for the subscription; the payment intent tracks the status of the provided payment method. To retrieve subscription.latest_invoice.payment_intent.status, you expand the latest_invoice child object of the response.

You should also receive an invoice.paid event. You can disregard this event for initial payment, but monitor it for subsequent payments. The invoice.paid event type corresponds to the payment_intent.status of succeeded, so payment is complete, and the subscription status is active.

Provision access to your service
Client and Server

To give the customer access to your service:

  1. Verify the subscription status is active.
  2. Check the product the customer subscribed to and grant access to your service. Checking the product instead of the price gives you more flexibility if you need to change the pricing or billing interval.
  3. Store the product.id and subscription.id in your database along with the customer.id you already saved.

On the frontend, you can implement these steps in the success callback after the subscription is created.

script.js
function onSubscriptionComplete(result) { // Payment was successful. if (result.subscription.status === 'active') { // Change your UI to show a success message to your customer. // Call your backend to grant access to your service based on // `result.subscription.items.data[0].price.product` the customer subscribed to. } }

It’s possible for a user to leave your application after payment is made and before this function is called. Continue to monitor the invoice.paid event on your webhook endpoint to verify that the payment succeeded and that you should provision the subscription.

This is also good practice because during the lifecycle of the subscription, you need to keep provisioning in sync with subscription status. Otherwise, customers might be able to access your service even if their payments fail.

Manage payment authentication
Client and Server

If you support payment methods that require customer authentication with 3D Secure, the value of latest_invoice.payment_intent.status is initially requires_action. The response from the createSubscription call looks like this:

{ "id": "sub_1ELI8bClCIKljWvsvK36TXlC", "object": "subscription", "status": "incomplete", ... "latest_invoice": { "id": "in_EmGqfJMYy3Nt9M", "status": "open", ... "payment_intent": { "status": "requires_action", "client_secret": "pi_91_secret_W9", "next_action": { "type": "use_stripe_sdk", ... }, ... } } }

In production, you’d monitor the invoice.payment_action_required event type.

To handle this scenario, on the frontend notify the customer that authentication is required to complete payment and start the subscription. Retrieve the client secret for the payment intent, and pass it in a call to stripe.confirmCardPayment.

script.js
function handlePaymentThatRequiresCustomerAction({ subscription, invoice, priceId, paymentMethodId, isRetry, }) { if (subscription && subscription.status === 'active') { // Subscription is active, no customer actions required. return { subscription, priceId, paymentMethodId }; } // If it's a first payment attempt, the payment intent is on the subscription latest invoice. // If it's a retry, the payment intent will be on the invoice itself. let paymentIntent = invoice ? invoice.payment_intent : subscription.latest_invoice.payment_intent; if ( paymentIntent.status === 'requires_action' || (isRetry === true && paymentIntent.status === 'requires_payment_method') ) { return stripe .confirmCardPayment(paymentIntent.client_secret, { payment_method: paymentMethodId, }) .then((result) => { if (result.error) { // Start code flow to handle updating the payment details. // Display error message in your UI. // The card was declined (that is, insufficient funds, card has expired, etc). throw result; } else { if (result.paymentIntent.status === 'succeeded') { // Show a success message to your customer. } } }) .catch((error) => { displayError(error); }); } else { // No customer action needed. return { subscription, priceId, paymentMethodId }; } }

This displays an authentication modal to your customers, attempts payment, then closes the modal and returns context to your application.

Make sure to monitor the invoice.paid event on your webhook endpoint to verify that the payment succeeded. It’s possible for users to leave your application before confirmCardPayment() finishes, so verifying whether the payment succeeded allows you to correctly provision your product.

Manage subscription payment failure
Client and Server

If the value of subscription.latest_invoice.payment_intent.status is requires_payment_method, the card was processed when the customer first provided card details, but payment then failed later—in a production scenario, if a customer’s card was stolen or canceled after the subscription was set up, for example. The example relies on the API response object so that you can test error handling. In production you’d monitor the invoice.payment_failed webhook event for payments after initial payment success.

Catch the error, let your customer know their card was declined, and return them to the payment form to try a different card.

script.js
function handleRequiresPaymentMethod({ subscription, paymentMethodId, priceId, }) { if (subscription.status === 'active') { // subscription is active, no customer actions required. return { subscription, priceId, paymentMethodId }; } else if ( subscription.latest_invoice.payment_intent.status === 'requires_payment_method' ) { // Using localStorage to manage the state of the retry here, // feel free to replace with what you prefer. // Store the latest invoice ID and status. localStorage.setItem('latestInvoiceId', subscription.latest_invoice.id); localStorage.setItem( 'latestInvoicePaymentIntentStatus', subscription.latest_invoice.payment_intent.status ); throw { error: { message: 'Your card was declined.' } }; } else { return { subscription, priceId, paymentMethodId }; } }

On the frontend, define the function to attach the new card to the customer and update the invoice settings. Pass the customer, new payment method, invoice, and price IDs to a backend endpoint.

script.js
function retryInvoiceWithNewPaymentMethod({ customerId, paymentMethodId, invoiceId, priceId }) { return ( fetch('/retry-invoice', { method: 'post', headers: { 'Content-type': 'application/json', }, body: JSON.stringify({ customerId: customerId, paymentMethodId: paymentMethodId, invoiceId: invoiceId, }), }) .then((response) => { return response.json(); }) // If the card is declined, display an error to the user. .then((result) => { if (result.error) { // The card had an error when trying to attach it to a customer. throw result; } return result; }) // Normalize the result to contain the object returned by Stripe. // Add the additional details we need. .then((result) => { return { // Use the Stripe 'object' property on the // returned result to understand what object is returned. invoice: result, paymentMethodId: paymentMethodId, priceId: priceId, isRetry: true, }; }) // Some payment methods require a customer to be on session // to complete the payment process. Check the status of the // payment intent to handle these actions. .then(handlePaymentThatRequiresCustomerAction) // No more actions required. Provision your service for the user. .then(onSubscriptionComplete) .catch((error) => { // An error has happened. Display the failure to the user here. // We utilize the HTML element we created. displayError(error); }) ); }

On the backend, define the endpoint for your frontend to call. The code updates the customer with the new payment method, and assigns it as the new default payment method for subscription invoices.

server.rb
# Set your secret key. Remember to switch to your live secret key in production. # See your keys here: https://dashboard.stripe.com/apikeys Stripe.api_key =
'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
post '/retry-invoice' do content_type 'application/json' data = JSON.parse request.body.read begin Stripe::PaymentMethod.attach( data['paymentMethodId'], { customer: data['customerId'] } ) rescue Stripe::CardError => e halt 200, { 'Content-Type' => 'application/json' }, { 'error': { message: e.error.message } }.to_json end # Set the default payment method on the customer Stripe::Customer.update( data['customerId'], invoice_settings: { default_payment_method: data['paymentMethodId'] } ) invoice = Stripe::Invoice.retrieve( { id: data['invoiceId'], expand: %w[payment_intent] } ) invoice.to_json end

Cancel the subscription
Client and Server

It’s common to allow customers to cancel their subscriptions. This example adds a cancellation option to the account settings page.

The example collects the subscription ID on the frontend, but you will most likely get this information from your database for your logged in user.

Account settings with the ability to cancel the subscription

script.js
function cancelSubscription() { return fetch('/cancel-subscription', { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subscriptionId: subscriptionId, }), }) .then(response => { return response.json(); }) .then(cancelSubscriptionResponse => { // Display to the user that the subscription has been canceled. }); }

On the backend, define the endpoint for your frontend to call.

server.rb
# Set your secret key. Remember to switch to your live secret key in production. # See your keys here: https://dashboard.stripe.com/apikeys Stripe.api_key =
'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
post '/cancel-subscription' do content_type 'application/json' data = JSON.parse request.body.read deleted_subscription = Stripe::Subscription.delete(data['subscriptionId']) deleted_subscription.to_json end

You should receive a customer.subscription.deleted event.

After the subscription is canceled, update your database to remove the Stripe subscription ID you previously stored, and limit access to your service.

When a subscription is canceled, it cannot be reactivated. Instead, collect updated billing information from your customer, update their default payment method, and create a new subscription with their existing customer record.

Test your integration

To make sure your integration is ready for production, you can work with the following test cards. Use them with any CVC, postal code, and future expiration date.

Card number What it does
Succeeds and immediately creates an active subscription.
Requires authentication. confirmCardPayment() will trigger a modal asking for the customer to authenticate. Once the user confirms, the subscription will become active. See manage payment authentication.
Always fails with a decline code of insufficient_funds. See create subscription step on how to handle this server side.
Succeeds when it initially attaches to Customer object, but fails on the first payment of a subscription with the payment_intent value of requires_payment_method. See the manage subscription payment failure step.

Check out the documentation for testing Billing for more information and ways to test your integration.

OptionalLet customers change their plans
Client and Server

OptionalPreview a price change
Client and Server

OptionalDisplay the customer payment method
Client and Server

Was this page helpful?
Questions? Contact us.
View developer tutorials on YouTube.
Check out our product changelog.
Powered by Markdoc
You can unsubscribe at any time. Read our privacy policy.
On this page
What you’ll build
How to model it on Stripe
Install Stripe libraries and tools
Test as you go
Create the business model
Create the Stripe customer
Collect payment information
Save payment details and create the subscription
Provision access to your service
Manage payment authentication
Manage subscription payment failure
Cancel the subscription
Test your integration
Let customers change their plans
Preview a price change
Display the customer payment method
Stripe Shell
Test mode
▗▄ ▄▟█ █▀▀ ▗▟████▙▖ ██████ ███▗▟█ ███ ███▗▟██▙▖ ▗▟█████▙▖ ███▖ ▀▀ ███ ███▀▀▀ ███ ███▀ ███ ███ ███ ▝▜████▙▖ ███ ███ ███ ███ ███ █████████ ▄▄ ▝███ ███ ▄ ███ ███ ███▄ ███ ███ ▄▄ ▝▜████▛▘ ▝▜███▛ ███ ███ ███▝▜██▛▘ ▝▜█████▛▘ ███ ▀▘
Welcome to the Stripe Shell! Stripe Shell is a browser-based shell with the Stripe CLI pre-installed. Login to Stripe docs and press Control + Backtick on your keyboard to start managing your Stripe resources in test mode. - View supported commands: - Find webhook events: - Listen for webhook events: - Call Stripe APIs: stripe [api resource] [operation] (e.g. )
The Stripe Shell is best experienced on desktop.
$