# Manage subscriptions on iOS

Accept recurring payments and manage entitlements with the BillingSDK for iOS.

Accept subscription payments, let customers manage their subscriptions, and manage [Entitlements](https://docs.stripe.com/billing/entitlements.md) directly in your iOS app with the [BillingSDK for iOS](https://github.com/stripe-samples/billing-ios-sdk). The SDK provides prebuilt UI components to display buy buttons, present the [customer portal](https://docs.stripe.com/customer-management/activate-no-code-customer-portal.md), and check entitlements to gate premium features.

## Before you begin

To use the BillingSDK for iOS, you’ll need:

- A backend server to create [Customer Sessions](https://docs.stripe.com/api/customer_sessions.md)
- A Stripe account with access to the private preview
- iOS 15.0 or later and macOS 12.0 or later
- Xcode 15.0 or later

## What you’ll build 

This guide shows you how to:

- Set up a server endpoint to create [Customer Sessions](https://docs.stripe.com/api/customer_sessions.md)
- Install and configure the BillingSDK for iOS
- Display buy buttons for subscription purchases
- Check entitlements to gate premium features
- Present the customer portal for subscription management
- Handle errors and manage session state

You can find a complete example app in the [BillingSDK for iOS repository](https://github.com/stripe-samples/billing-ios-sdk).

## Set up your backend [Server]

Create a server endpoint that [generates Customer Sessions](https://docs.stripe.com/api/customer_sessions/create.md) for authenticated users. These sessions securely authenticate your iOS app with Stripe’s billing APIs.

### Create a customer session endpoint

The endpoint needs to:

1. Verify that the user is authenticated
1. Create or retrieve the Stripe [Customer ID](https://docs.stripe.com/api/customers/object.md) for the user
1. Create a Customer Session with the required components enabled
1. Return the session details to your app

Here’s an example implementation:

```javascript
import 'dotenv/config';
import express from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
  apiVersion: '2025-07-30.basil',
});

const app = express();
app.use(express.json());

app.post('/authenticate', async (req, res) => {
  try {
    // Replace with your auth; return 401 if the user is not logged in
    const user = authUser(req);
    if (!user) {
      res.status(401).json({ error: { type: 'no_session_present' } });
      return;
    }

    const customerSession = await stripe.customerSessions.create({
      customer: user.stripeCustomerId,
      components: {
        buy_button: { enabled: true },
        active_entitlements: { enabled: true },
        customer_portal: { enabled: true },
      } as any,
    });

    res.json({
      clientSecret: customerSession.client_secret,
      expiresAt: customerSession.expires_at,
      customer: customerSession.customer as string,
    });
  } catch (err) {
    res.status(500).json({ error: { message: 'Internal server error' } });
  }
});

app.listen(3000);
```

### Response format

The endpoint must return these fields for successful authentication:

| Field          | Type   | Description                                              |
| -------------- | ------ | -------------------------------------------------------- |
| `clientSecret` | string | The Customer Session secret used to authenticate the SDK |
| `expiresAt`    | number | Expiration timestamp in seconds since epoch              |
| `customer`     | string | The Stripe Customer ID                                   |

When authentication fails, return HTTP 401 to trigger the SDK’s unauthenticated state.

## Install and configure the SDK [iOS]

The BillingSDK for iOS provides a native iOS experience for subscription management and feature access control.

### Add the SDK to your project

During private preview, install the BillingSDK package:

1. Open your project in Xcode
1. Select **File** → **Add Package Dependencies**
1. Type `https://github.com/stripe-samples/billing-ios-sdk` into the **Search or Enter Package URL** field
1. Select `BillingSDK` and click **Add Package**

### Configure the SDK

Initialize the SDK in a shared class to handle the BillingSDK setup and authentication:

```swift
import Foundation
import BillingSDK
import SwiftUI

class BillingManager {
    static let shared = BillingManager()

    public let billing: BillingSDK

    private init() {
        // Initialize with your publishable key and set how long entitlements can be stale before needing to refresh.
        let configuration = BillingSDK.Configuration(
          publishableKey: "pk_test_…",
          maximumStaleEntitlementsDuration: TimeInterval(60 * 5)
        )
        self.billing = BillingSDK(configuration: configuration)

        // Set up authentication (see below for implementation)
        setupCustomerSessionProvider()

        // Add entitlement change listener (see below for implementation)
        setupEntitlementListener()
    }
}
```

> The SDK handles thread safety internally—you can safely call its methods from any thread.

### Set up the Customer Session provider

Add the authentication method to your billing manager:

```swift
import Foundation
import BillingSDK

extension BillingManager {
    func setupCustomerSessionProvider() {
        billing.setCustomerSessionProvider { [weak self] () async -> UBCustomerSessionDetails? in
            await self?.fetchCustomerSession()
        }
    }

    private func fetchCustomerSession() async -> UBCustomerSessionDetails? {
        // Configure request to your backend endpoint
        guard let url = URL(string: "https://your-backend.example.com/customer-session") else {
            print("Invalid URL")
            return nil
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        // Send any additional headers needed to authenticate the user, for example, an auth token.
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")

        do {
            // Make the request
            let (data, response) = try await URLSession.shared.data(for: request)

            guard let httpResponse = response as? HTTPURLResponse else {
                print("Invalid response format")
                return nil
            }

            switch httpResponse.statusCode {
            case 200:
                // Parse the successful response
                let decoder = JSONDecoder()
                decoder.dateDecodingStrategy = .secondsSince1970
                return try decoder.decode(UBCustomerSessionDetails.self, from: data)

            case 401:
                // User not authenticated - SDK will enter unauthenticated state
                print("Authentication required")
                return nil

            default:
                print("Unexpected status code: \(httpResponse.statusCode)")
                return nil
            }
        } catch {
            print("Session provider error: \(error.localizedDescription)")
            return nil
        }
    }

    // Call this method when your user signs out
    func handleSignOut() async {
        await billing.reset() // Clear session data and caches
    }
}
```

> The BillingSDK automatically calls your session provider whenever it needs to authenticate with Stripe. When the provider returns `nil`, the SDK enters an unauthenticated state.

## Display subscription buy buttons [iOS]

Buy buttons provide a prebuilt UI element to let customers purchase subscriptions. Each button is linked to a specific product and price in your Stripe account.

### Create buy buttons in the Dashboard

Follow [this guide](https://docs.stripe.com/payment-links/buy-button.md) to create buy buttons in the Dashboard. If you plan to use the entitlement checking functionality, ensure that the products you’ve created have [Entitlements](https://docs.stripe.com/billing/entitlements.md) attached to them.

### Display a buy button

```swift
import SwiftUI
import BillingSDK

struct SubscriptionView: View {
    @State private var buyButton: BuyButton?
    @State private var isLoading = true
    @State private var errorMessage: String?

    // Replace with your buy button ID from the Dashboard
    let buttonId = "buy_btn_1Abcdef123456"

    var body: some View {
        VStack {
            if isLoading {
                ProgressView("Loading subscription options...")
            } else if let buyButton = buyButton {
                // Display the prebuilt buy button UI
                // Alternatively the BuyButton class also provides all the data to render
                // a custom UI element.
                buyButton.view()
                    .frame(height: 56)
                    .padding(.horizontal)
            } else if let errorMessage = errorMessage {
                Text("Error: \(errorMessage)")
                    .foregroundColor(.red)
                Button("Retry") {
                    loadBuyButton()
                }
            }
        }
        .onAppear {
            loadBuyButton()
        }
    }

    func loadBuyButton() {
        isLoading = true
        errorMessage = nil

        Task {
            do {
                // Fetch the button from Stripe
                buyButton = try await BillingManager.shared.billing.getBuyButton(id: buttonId)
                isLoading = false
            } catch {
                errorMessage = "Failed to load buy button"
                isLoading = false
            }
        }
    }
}
```

> Buy buttons work even when users aren’t authenticated. The SDK creates a new Stripe Customer during purchase if needed.

## Check and validate entitlements [iOS]

Entitlements let you control access to premium features based on a customer’s active subscriptions.

### Check a specific entitlement

Verify if a user has access to a specific feature:

```swift
func checkPremiumAccess() async {
    do {
        let hasPremiumAccess = try await BillingManager.shared.billing.hasEntitlement(lookupKey: "premium_tier")

        if hasPremiumAccess {
            // User has premium access - enable features
            unlockPremiumFeatures()
        } else {
            // User doesn't have access - show upsell
            showPremiumUpsell()
        }
    } catch {
        showErrorMessage("Couldn't verify entitlements status")
    }
}
```

### Get all active entitlements

Retrieve all entitlements the customer has access to:

```swift
func loadUserEntitlements() async {
    do {
        // Force refresh ensures we get the latest data from the server
        let entitlements = try await BillingManager.shared.billing.getActiveEntitlements(forceRefresh: true)

        // Map entitlements to app features
        let features = entitlements.map { $0.lookupKey }
        updateAvailableFeatures(features)

    } catch {
        handleError(error)
    }
}
```

### Listen for entitlement changes

Set up a listener to be notified when entitlements change:

```swift
import Foundation
import BillingSDK

extension BillingManager {
    func setupEntitlementListener() {
        billing.onEntitlementsChanged { updatedEntitlements in
            // Update UI based on new entitlements
            DispatchQueue.main.async {
                self.updateAppFeatures(based: updatedEntitlements)
                self.refreshUI()
            }
        }
    }
}
```

> When no session is present, `getActiveEntitlements()` returns an empty array.

## Present the customer portal [iOS]

The customer portal lets subscribers manage their subscriptions, payment methods, and billing information.

### Show the customer portal

Present the portal when users need to manage their subscription:

```swift
func manageSubscriptionTapped() {
    Task {
        do {
            let portal = try await BillingManager.shared.billing.getCustomerPortal()

            // Show portal within your app (recommended)
            portal.presentCustomerPortal(from: self)

        } catch {
            if let billingError = error as? BillingSDKError {
                switch billingError {
                case .unauthenticated:
                    // User needs to log in first
                    showLoginPrompt()
                default:
                    // Handle other errors
                    showErrorMessage("Could not load subscription management")
                }
            } else {
                showErrorMessage("Could not load subscription management")
            }
        }
    }
}
```

> The customer portal requires an active authenticated session. If the user isn’t logged in, the SDK throws an `.unauthenticated` error.

### External redirection (optional)

You can also open the portal in a browser:

```swift
let portal = try await BillingManager.shared.billing.getCustomerPortal()
portal.redirectToCustomerPortal() // Opens in default browser
```

## Session management [iOS]

### Session management

Reset the SDK when users sign out to clear session data and caches:

```swift
func signOut() async {
    // Clear your app's auth state
    UserDefaults.standard.removeObject(forKey: "auth_token")

    // Reset the BillingSDK state
    await BillingManager.shared.billing.reset()

    // Navigate to login screen
    showLoginScreen()
}
```

## Test your integration

During development, use your [sandbox API keys](https://docs.stripe.com/keys.md#obtain-api-keys) and sandbox Customer IDs to avoid creating real charges.

### Testing scenarios

Test these common scenarios:

| Scenario                  | Steps                                         |
| ------------------------- | --------------------------------------------- |
| New subscription purchase | Present a buy button to a new user            |
| Subscription management   | Use the customer portal to change plans       |
| Entitlement verification  | Check a premium feature with `hasEntitlement` |
| Error handling            | Test with invalid keys or expired sessions    |
| Sign out flow             | Verify `billing.reset()` clears cached data   |

## See also

- [Entitlements](https://docs.stripe.com/billing/entitlements.md)
- [Configure the customer portal](https://docs.stripe.com/customer-management/integrate-customer-portal.md)
- [How subscriptions work](https://docs.stripe.com/billing/subscriptions/overview.md)
