Create fixed-price subscriptions
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
How to model it on Stripe
API object definitions
Install Stripe libraries and tools
Install the Stripe client of your choice:
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.
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 goServer
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.
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 modelStripe 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.
Create the Stripe customerClient and Server
In this guide, you create a customer from the provided email.
On your application frontend, pass the customer email to a backend endpoint.
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 informationClient
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.
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.
<head> <title>Subscription prices</title> <script src="https://js.stripe.com/v3/"></script> </head>
Create an instance of Elements with the following JavaScript:
// 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(
); let elements = stripe.elements();'pk_test_TYooMQauvdEDq54NiTphI7jx'
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.
<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:
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.
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 subscriptionClient 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:
var form = document.getElementById('subscription-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.
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.
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 serviceClient and Server
To give the customer access to your service:
- Verify the subscription status is
active
. - 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.
- Store the
product.id
andsubscription.id
in your database along with thecustomer.id
you already saved.
On the frontend, you can implement these steps in the success callback after the subscription is created.
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 authenticationClient 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
.
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 failureClient 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.
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.
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.
Cancel the subscriptionClient 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.
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.
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.