Stripe | Financial Infrastructure to Grow Your Revenue

Stripe | Financial Infrastructure to Grow Your Revenue

4466 articles

Provide subscription management on iOS with a customer portal page


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:

  1. In Xcode, select File > Add Package Dependencies… and enter https://github. the relevant part of the product as the repository URL.
  2. Select the latest version number from our releases page .
  3. 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
 });
})

Universal links allow the customer portal to deep link into your app. To configure a universal link:

  1. Add an apple-app-site-association file to your domain.
  2. Add an Associated Domains entitlement to your app.
  3. 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

  1. Open the Signing & Capabilities pane of your app’s target.
  2. Click + Capability , then select Associated Domains .
  3. 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:

  1. Calls your server-side endpoint to create a portal session.
  2. Returns the portal page URL to the client.
  3. 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
})

See also

Last verified 2026-09-24

Is this helpful?