An iOS Swift Package that helps you sell and manage subscriptions and feature access (entitlements) with Stripe Billing.
- Fetch and render Buy Buttons
- Present the Customer Portal for subscription management
- Read active entitlements and gate features
- Local caching with refresh and offline fallback
For a detailed guide on how to use this, please visit the Stripe docs.
- This SDK is an experimental preview.
- It requires your Stripe account to be opted in to private preview APIs.
-
example-backend/
- Node.js Express server that exposes
/authenticate
. - Requires
STRIPE_SECRET_KEY
andSTRIPE_CUSTOMER_ID
in the environment. - Uses a private preview Stripe API version (for example
2025-07-30.basil
). - Run:
cd example-backend
npm install
npm run dev
- Node.js Express server that exposes
-
ExampleApp/
- SwiftUI app showing end-to-end usage: Buy Button, Entitlements, and Customer Portal.
- Update
ExampleApp/Constant.swift
with your values (stripePublishableKey
,stripeCustomerId
,stripeBuyButtonId
, backend URL, and entitlement lookup keys). - Open
ExampleApp.xcodeproj
and run the app. Ensure the backend is running.
- Open your project in Xcode
- Select File → Add Package Dependencies
- Type
https://github.com/stripe-samples/billing-ios-sdk
into the Search or Enter Package URL field - Select
BillingSDK
and click Add Package
import BillingSDK
let configuration = BillingSDK.Configuration(
// Your Stripe publishable key
publishableKey: "pk_test_…",
maximumStaleEntitlementsDuration: TimeInterval(60 * 5)
)
let billing = BillingSDK(configuration: configuration)
- Called by the SDK when it needs to authenticate.
- Return
nil
when the user isn’t logged in; return your backend payload when they are.
billing.setCustomerSessionProvider {
// Call your backend (example uses POST /authenticate) and return a UBCustomerSessionDetails
// The endpoint path and auth are up to you; the app provides a closure that calls it.
return UBCustomerSessionDetails(
customer: "customer_id",
clientSecret: "client_secret",
expiresAt: Date()
)
}
let buyButton = try await billing.getBuyButton(id: "buy_btn_…")
// SwiftUI
buyButton.view()
// You can also render your own buy button based on the BuyButton data model.
let entitlements = try await billing.getActiveEntitlements(forceRefresh: true)
let hasPremium = try await billing.hasEntitlement(lookupKey: "premium")
billing.onEntitlementsChanged { updated in
// Update UI as entitlements change
}
let portal = try await billing.getCustomerPortal()
// UIKit: present in-app Safari (preferred)
portal.presentCustomerPortal(from: presentingViewController)
// Or open externally
portal.redirectToCustomerPortal()
Behavior notes:
- If no session is present,
getActiveEntitlements
returns an empty array. - Buy button can be rendered without a session, this will result in a new Stripe Customer getting created.
- Customer Portal require a session and will error if unauthenticated.
To clear the cache and reset the SDK to its initial state, call .reset()
:
await billing.reset()
Your backend should:
- Verify the user is authenticated
- Create or retrieve the Stripe Customer ID for the user
- Create a Customer Session with the required components enabled
- Return the session details to your app
For example:
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 hasSessionPresent = req.body.hasSessionPresent !== false;
if (!hasSessionPresent) {
res.status(401).json({ error: { type: 'no_session_present' } });
return;
}
const customerSession = await stripe.customerSessions.create({
customer: process.env.STRIPE_CUSTOMER_ID || '',
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, // seconds since epoch
customer: customerSession.customer as string,
});
} catch (err) {
res.status(500).json({ error: { message: 'Internal server error' } });
}
});
app.listen(3000);