Create per-seat subscriptions with Elements
For new users, it’s highly recommended that you use Payment Element instead of using Stripe Elements as described in this guide. Payment Element provides a low-code integration path with built-in conversion optimizations. For instructions, see Build a subscription.
This guide walks you through how to create per-seat subscriptions for a messaging service that offers a basic option with a single price per seat, and a premium option with discounted pricing based on the number of seats purchased. It uses Stripe Elements to create a custom payment form you embed in your application.
You can find code for an example implementation on GitHub.
Other guides that explain how to build subscriptions with different business models:
- Fixed-price subscriptions, where everyone pays the same amount for a single product or service
- Metered billing, where customers pay depending on how much they use of a service such as data storage, or calls to an API
This guide is different in that you pass the quantity to the subscription object. Like the metered billing guide, it also shows how to use tiers, in this case to offer discounted pricing for larger quantities.
What you’ll build
This guide shows you how to:
- Model your subscriptions with Products and Prices
- Create a signup flow
- Collect payment information
- Create Subscriptions and charge customers
- Handle payment errors
- Preview the cost of adding or removing seats, or changing the plan
- 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.
Set up webhooksServer
Because subscriptions are asynchronous, we recommend setting up webhooks to receive events from Stripe instead of polling for status updates. You can set up a webhook endpoint in the Dashboard, or with the Webhook Endpoints API.
The Stripe CLI provides a listen command for testing event monitoring during development.
Get started with these events:
Webhook | Why you need it |
---|---|
invoice.payment_failed | If the payment fails or the customer does not have a valid payment method, an invoice.payment_failed event is sent, and the subscription status becomes past_due . Use this event to notify your user that their payment has failed and request new card details. |
invoice.paid | Use this event to provision services. Store the status in your database to reference when a user accesses your service to avoid hitting rate limits. This event is also important for monitoring the status of payments that require customer authentication with 3D Secure. |
Here’s how to set up your webhook handler and verify the signature of the event.
Learn about other events your webhook can listen for.
Create the business modelStripe CLI or Dashboard
You create your products and their pricing options with the Stripe CLI or in the Dashboard. A service with two different options needs a product and a price for each option. For the premium service, the price includes tiers to represent the discount levels.
In this sample, each product bills at monthly intervals. The price for the basic product is 5 USD per seat, and the price for the premium product starts at 15 USD per seat and goes down as the customer purchases more seats.
Create the Stripe customerClient and Server
In this sample, 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 // result.setupIntent.client_secret is used to create the payment method return result; }); }
On the backend, define the endpoint to create the customer object.
We recommend that you create the customer object together with your app’s user account. You should also store the response in your database, for example, customer.id
as stripeCustomerId
in your users
collection or table. This is to limit the number of calls you make to Stripe, for performance efficiency and to avoid possible rate limiting.
The server APIs this guide demonstrates are not secure by default. Make sure you properly enforce CORS by allowing access to these APIs only from your frontend application.
Example customer response:
{ "id": "cus_HAwLCi6nxPYcsl", "object": "customer", "address": null, "balance": 0, "created": 1588007418, "currency": "usd", "default_source": null, "delinquent": false, "description": "My First Test Customer",
Collect payment informationClient
Let your new customer choose a plan and provide payment information. In the sample, the customer chooses between Basic and Premium, and specifies the number of seats.
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.
function createPaymentMethod(cardElement, customerId, priceId) { return stripe .createPaymentMethod({ type: 'card', card: cardElement, }) .then((result) => { if (result.error) { displayError(error); } 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, but you should store whatever your application will frequently access.
{ "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",
To verify payment status, check the value of subscription.latest_invoice.payment_intent.status
. This field describes the status of the payment intent for the latest subscription invoice. The invoice tracks overall payment status for the subscription; the payment intent tracks the status of an individual payment. To get this value, you must expand the child objects of the reponse.
If the value of subscription.latest_invoice.payment_intent.status
is succeeded
, payment is completed, and the subscription status should be 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. Make sure 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 subscription.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", ... }, ... } } }
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. // There's a risk of the customer closing the window before the callback. // We recommend setting up webhook endpoints later in this guide. return { priceId: priceId, subscription: subscription, invoice: invoice, paymentMethodId: paymentMethodId, }; } } }) .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 after the payment method was attached to the customer object. Here’s how to handle the situation.
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. The sample adds a cancellation option to the account settings page.
The sample 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.
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, with the payment_intent value of requires_action . The confirmCardPayment() triggers a modal that asks the customer to authenticate. When the user confirms, the subscription is active. See manage payment authentication. | |
Always fails with a decline code of insufficient_funds . To handle this error on the backend, see create subscription. | |
Succeeds when first attached to the customer object, but fails on first payment of the subscription, with the payment_intent value of requires_payment_method . See manage subscription payment failure. |
See the documentation on testing Billing for more information and ways to test your integration.