Provide subscription management on iOS with a customer portal page
Set up a customer portal and open it in a browser from your app.
For digital products and content, including subscriptions, sold in the United States or European Economic Area (EEA), your app can accept Apple Pay by redirecting to an external payment page.
This guide describes how to configure a customer portal for subscription management and redirect your customers to it from your app.
Link out of app to manage subscriptions and payment methods
What you’ll build
Note
This guide only describes subscription management. If you’re setting up subscription purchases, see Accept payments for digital goods on iOS with a prebuilt payment page.
This guide shows you how to:
- Set up a customer portal page that customers can use to manage subscriptions
- Use universal links to redirect users back to your app from the customer portal
- Monitor webhooks to update your customer’s subscription status
What isn’t covered
This guide demonstrates how to set up a Stripe customer portal and link to it from your app. It doesn’t cover:
- Subscription purchases : To use Stripe Checkout to sell in-app goods and subscriptions, see Accept payments for digital goods on iOS with a prebuilt payment page .
- User authentication : If you don’t have an existing authentication provider, you can use a third-party provider, such as Sign in with Apple or Firebase Authentication .
- Native in-app purchases : To implement in-app purchases using StoreKit, visit Apple’s in-app purchase guide .
Configure the portal
First, you need to register for a Stripe account.
Before you integrate the customer portal, use the Dashboard to define what your users can do with the portal. Choose your settings for sandboxes and live mode, based on your product and price catalog.
Common mistake
If you’re using the customer portal with Stripe Connect, make sure you configure the customer portal for the platform, not a connected account.
If you want to create multiple portal configurations for different sets of customers (or if you’re a Connect platform and want to manage configurations for your connected accounts), you can do so using the API:
Command Line
Select a language
cURL
Stripe CLI
Ruby
Python
PHP
Java
Node.js
Go
.NET
No results
Set a product catalog
If you allow customers to upgrade, downgrade, or change the quantities of their subscriptions, you must also set a product catalog. This includes the products and prices that your customers can upgrade or downgrade to, and the subscriptions they can update quantities on. See how to create a product for more details about creating products and prices. If you’re using the customer portal for invoicing only, you don’t need to set a product catalog.
The portal displays the following attributes of your product catalog:
- Product name and description —These attributes are editable in the Dashboard and API.
- Quantity restrictions per product —These attributes are editable in the Dashboard.
- Price amount, currency, and billing interval —These attributes are fixed, and you can only set them when you create them in the Dashboard and API.
Enable tax ID collection
If you use Stripe Tax to automatically collect taxes for subscriptions or invoices, you can let customers set and update their tax IDs in the customer portal. Stripe Billing adds the tax IDs to the customers’ invoices. To allow customers to set their tax IDs, go to the Customer portal settings and toggle on Tax ID. For more information, see how customer tax IDs work with subscriptions and invoices.
Learn how to set up Stripe Tax, collect taxes for recurring payments, collect taxes in your custom payment flows and set tax rates for line items and invoices.
Preview and test
As you configure your settings, click Preview to preview the portal. This launches a read-only version of the portal that lets you see how your customers might manage their subscriptions and billing details.
After saving your settings, you can launch the portal and test it by using a customer in a sandbox. Go to a customer in the Dashboard, click Actions, and then select Open customer portal.
You can only preview the portal as a read-only version when your Dashboard is in a sandbox. If you can’t preview and test the portal, check your settings to make sure that your configuration is saved in a sandbox. For previewing and testing to work, you also need to have edit permissions in the Dashboard.
Set up Stripe Server-side
Server-side
Command Line
Select a language
Ruby
Python
PHP
Java
Node.js
Go
.NET
No results
# Available as a gem
sudo gem install stripe
Gemfile
Select a language
Ruby
Python
PHP
Java
Node.js
Go
.NET
No results
# If you use bundler, you can add this line to your Gemfile
gem 'stripe'
Next, install the Stripe CLI. The CLI provides the webhook testing that you need.
Install the Stripe CLI with npm:
Command Line
npm install -g @stripe/cli@latest
After installation, log in to your Stripe account:
Command Line
stripe login
After you install the CLI, you can also install agent tooling, or set up autocompletion.
Note
For more installation options for Windows, macOS, Linux, and Docker, see the Stripe CLI readme on GitHub.
Client-side
The Stripe iOS SDK is open source, fully documented, and compatible with apps supporting iOS 13 or above.
To install the SDK, follow these steps:
- In Xcode, select File > Add Package Dependencies… and enter https://github. the relevant part of the product as the repository URL.
- Select the latest version number from our releases page .
- Add the StripePaymentSheet product to the target of your app .
Note
For details on the latest SDK release and past versions, see the Releases page on GitHub. To receive notifications when a new release is published, watch releases for the repository.
You also need to set your publishable key so that the SDK can make API calls to Stripe. To get started quickly, you can hardcode this on the client while you’re integrating, but fetch the publishable key from your server in production.
// Set your publishable key: remember to change this to your live publishable key in production
// See your keys here: https://dashboard.stripe.com/apikeys
STPAPIClient.shared.publishableKey = "pk_test_TYooMQauvdEDq54NiTphI7jx"
Create a portal session Server-side
When a customer wants to make changes to their subscription, generate a URL for the portal page using their Stripe customer ID through the portal session API.
Node.js
// Don't put any keys in code. See https://docs.stripe.com/keys-best-practices.
// Find your keys at https://dashboard.stripe.com/apikeys.
const stripe = require('stripe')('sk_test_BQokikJOvBiI2HlWgH4olfQ2');
app.get('/customer_portal_url', async (req, res) => {
// Replace this with your actual customer lookup logic
const customerId = 'cus_...'; // Get this from your database
const billingSession = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://example.com/portal_redirect',
});
res.json({
url: billingSession.url
});
})
Set up universal links Client-side Server-side
Universal links allow the customer portal to deep link into your app. To configure a universal link:
- Add an apple-app-site-association file to your domain.
- Add an Associated Domains entitlement to your app.
- Add a fallback page for your portal redirect URLs.
Define the associated domains
Add a file to your domain at .well-known/apple-app-site-association to define the URLs your app handles. Prepend your App ID with your Team ID, which you can find on the Membership page of the Apple Developer Portal.
.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [ "A28BC3DEF9.com.example.MyApp1",
"A28BC3DEF9.com.example.MyApp1-Debug" ],
"components": [
{
"/": "/checkout_redirect*",
"comment": "Matches any URL whose path starts with /checkout_redirect"
}
]
}
]
}
}
You must serve the file with the related setting type application/json. Use curl -I to confirm the content type.
Command Line
See Apple’s page on supporting associated domains for more details.
Add an Associated Domains entitlement to your app
- Open the Signing & Capabilities pane of your app’s target.
- Click + Capability , then select Associated Domains .
- Add an entry for applinks:example. com to the Associated Domains list.
For more information on universal links, see Apple’s Universal Links for Developers page.
Although iOS intercepts links to the URLs defined in your apple-app-site-association file, you might encounter situations where the redirect fails to open your app.
Make sure to create a fallback page at your return_url. For example, you can define a custom URL scheme for your app and use it to link back in case the universal link fails.
Open the customer portal in Safari Client-side
Add a button to open the customer portal in your app. This button:
- Calls your server-side endpoint to create a portal session.
- Returns the portal page URL to the client.
- Opens the URL in Safari.
CheckoutView.swift
import Foundation
import SwiftUI
import StoreKit
struct SubscriptionManagementView: View {
@EnvironmentObject var myBackend: MyServer
var body: some View {
// Check if payments are blocked by Parental Controls on this device.
if !SKPaymentQueue.canMakePayments() {
Text("Payments are disabled on this device.")
} else {
Button {
myBackend.createCustomerPortalSession { url in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
} label: {
Text("Manage subscriptions")
}.onOpenURL { url in
// Handle the universal link from the customer portal.
// Implement any necessary behavior, such as refreshing the customer's subscription status.
}
}
}
}
Handle changes to customer subscription status Server-side
When customers make changes to their subscription status through the customer portal, Stripe sends you webhooks, such as customer.subscription.created, customer.subscription.deleted, and customer.subscription.updated. For a full list of events and information about them, see Using webhooks with subscriptions. Make sure you handle all events necessary to accurately monitor the statuses of the subscriptions you’ve configured.
For testing purposes, you can monitor events in the Dashboard or using the Stripe CLI. For production, set up a webhook endpoint and subscribe to appropriate event types. If you don’t know your the related setting key, click the webhook link in the Dashboard to view it.
server.js
Node.js
const express = require('express');
const app = express();
// Set your Stripe API key. Don't put any keys in code.
// See https://docs.stripe.com/keys-best-practices.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
app.post('/webhook', async (req, res) => {
let data;
let eventType;
// Check if webhook signing is configured.
const webhookSecret = "{{STRIPE_WEBHOOK_SECRET}}"
if (webhookSecret) {
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
let signature = req.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
webhookSecret
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed.`);
return res.sendStatus(400);
}
// Extract the object from the event.
data = event.data;
eventType = event.type;
} else {
// Webhook signing is recommended, but if the secret is not configured in `config.js`,
// retrieve the event data directly from the request body.
data = req.body.data;
eventType = req.body.type;
}
switch (eventType) {
case 'customer.subscription.created': {
const subscription = event.data.object;
const customerId = subscription.customer;
myUserDB.setUserSubscriptionIsActive(customerId, true);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
const customerId = subscription.customer;
myUserDB.setUserSubscriptionIsActive(customerId, false);
break;
}
// Add other relevant event types as needed
}
res.sendStatus(200); // Acknowledge receipt of the webhook
})
