Accept payments for digital goods on iOS with your own checkout page
Open your own checkout to sell in-app digital goods and subscriptions using Payment Element.
In some countries, you can link to an external website to accept payments on iOS. This guide describes how to sell a subscription in your app by redirecting to your own checkout page using Elements. If you already have your own checkout page with Elements, see Set up universal links.
Note
Contact our sales team if your business is new to Stripe, processes a high volume of payments, and has advanced integration needs.
What you’ll build 
Note
This guide only describes the process for selling in-app digital goods. If you sell any of the following use the native iOS payment guide instead:
- Physical items
- Goods and services intended for consumption outside your app
- Real-time person-to-person services between two individuals
This guide shows you how to:
- Collect payment information with your own checkout page using Elements.
- Model your subscriptions with Products, Prices, and Customers.
- Use universal links to redirect directly to your app from Checkout.
- Monitor webhooks to update your customer’s in-app subscriptions.
Set up StripeServer-side
First, register for a Stripe account.
Then add the Stripe API library to your backend:
Next, install the Stripe CLI. The CLI provides the required webhook testing, and you can run it to create your products and prices.
For additional install options, see Get started with the Stripe CLI.
Create products and prices
Create your products and their prices in the Dashboard or with the Stripe CLI.
This example uses a single product and price to represent a subscription product with a $9.99 USD monthly price.
Navigate to the Add a product page and create a subscription product with a 9.99 USD monthly price.
After you create the price, record the price ID so you can use it in subsequent steps. Price IDs look like this: price_
.
Next, click Copy to live mode to clone your product from a testing environment to live mode.
Create customersServer-side
Each time your customer goes to your checkout page, create a Customer object for your customer if one doesn’t already exist.
Your server needs to handle:
- Customer creation (if a matching Stripe Customer doesn’t exist yet).
- Subscription creation in an
incomplete
state. - Returning the PaymentIntent client secret to the front end.
- Webhook handling so you can update your customer’s subscription status in your own database.
Warning
Store an association on your server between the user account and the Stripe customer ID. Without a way to associate a customer with a purchase, your customers can’t recover their purchases.
If the customer changes their email on the checkout page, the Customer object updates with the new email.
Create a SubscriptionServer-side
When creating a subscription to use the Payment Element, you typically pass payment_
. This tells Stripe to create a subscription in incomplete
status and generate a Payment Intent for the initial payment.
Note
Store the subscription.
in your database to manage future subscription events such as cancellation, upgrades, and downgrades.
Note
Apple Pay is enabled by default and automatically appears in the Payment Element when a customer uses a supported device and has saved at least one card in the Wallet app. You can accept additional payment methods using the payment_
property. See payment methods overview for more details.
Set up universal links
Universal links allow your checkout page to deeply 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 checkout redirect URLs.
Define the associated domains
Add a file to your domain at .
to define the URLs that your app handles. Prepend your App ID with your Team ID, which you can find on the Membership page of the Apple Developer Portal.
{ "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" } ] } ] } }
Warning
You must serve the file with MIME type application/json
. Use curl -I
to confirm the content type.
curl -I https://example.com/.well-known/apple-app-site-association
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.
to the Associated Domains list.com
For more information on universal links, see Apple’s universal links documentation.
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.
Create a fallback page at your success
and cancel
URLs. For example, you can have a /checkout_
page and a /checkout_
page.
Open Checkout in SafariClient-side
Add a checkout button to your app. This button opens your custom checkout page in Safari.
import Foundation import SwiftUI import StoreKit struct BuySubscriptionsView: View { @EnvironmentObject var myBackend: MyBackend @State var paymentComplete = false 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 { if paymentComplete { Text("Payment complete!") } else { Button { UIApplication.shared.open("https://example.com/checkout", options: [:], completionHandler: nil) } label: { Text("Subscribe") }.onOpenURL { url in // Handle the universal link from Checkout. if url.absoluteString.contains("success") { // The payment was completed. Show a success // page and fetch the latest customer entitlements // from your server. paymentComplete = true } } } } } }
Redirect back to your appServer-side
With Elements, make sure you redirect users back to your app (using the registered universal link) on a successful payment confirmation.
Handle order fulfillmentServer-side
When the user completes the initial payment or when subsequent recurring payments occur, Stripe sends events such as:
invoice.
payment_ succeeded customer.
subscription. updated invoice.
payment_ failed
Listen for these events in your webhook endpoint. For example:
To test your integration, you can monitor events in the Dashboard or using the Stripe CLI. When developing in production, set up a webhook endpoint and subscribe to appropriate event types. If you don’t know your STRIPE_
key, click the webhook in the Dashboard to view it.
Testing 
To test your that your checkout button works, do the following:
- Click the checkout button, which redirects you to your checkout with Stripe’s Payment Element.
- Enter the test number , a three-digit CVC, a future expiration date, and any valid postal code.
- Tap Pay.
- The
invoice.
webhook fires, and Stripe notifies your server about the transaction.payment_ succeeded - You’re redirected back to your app.
If your integration isn’t working, see additional testing resources.
OptionalAdditional testing resources
There are several test cards you can use to make sure your integration is ready for production. Use them with any CVC, postal code, and future expiration date.
Number | Description |
---|---|
Succeeds and immediately processes the payment. | |
Complete 3D Secure 2 authentication for a successful payment. | |
Always fails with a decline code of insufficient_ . |
For the full list of test cards see the testing guide.
Test universal links
If your universal link doesn’t redirect from your checkout page to your app, check the SharedWebCredentials
logs for errors.
Add a debug parameter to the Associated Domains entitlement
- Open the Signing & Capabilities pane of your app’s target.
- Add the
?mode=developer
flag to the entry for your associated domain. (Example:applinks:example.
)com?mode=developer
Set the device to developer mode.
- Run an app from Xcode on your device to enable the developer menu.
- On your iPhone, open Settings, tap Developer, and enable Associated Domains Development.
Delete and reinstall your app. This causes iOS to re-fetch the apple-app-site-association file.
Complete the checkout flow in your app.
Checkout redirects you to your app. If it doesn’t, take a sysdiagnose.
Simultaneously press the volume up, volume down, and power buttons for 1 second, then release. You’ll feel a short vibration, but you won’t see any visual feedback.
Wait 5 minutes, then go to Settings > Privacy > Analytics & Improvement > Analytics Data, and scroll to the last sysdiagnose file in the list.
Tap the share button to AirDrop the file to your computer.
Open the sysdiagnose archive, then open
swcutil_
show. txt Search this file for your app’s ID. You’ll see a section with debugging information for your app, including an error if available.
Service: applinks App ID: Y28TH9SHX7.com.stripe.FruitStore App Version: 1.0 App PI: <LSPersistentIdentifier 0x115e1a390> { v = 0, t = 0x8, u = 0xc98, db = E335D78F-D49E-4F19-A150-F657E50DEDAE, {length = 8, bytes = 0x980c000000000000} } Domain: example.com?mode=developer User Approval: unspecified Site/Fmwk Approval: unspecified Flags: developer Last Checked: 2021-09-23 18:16:58 +0000 Next Check: 2021-09-23 21:21:34 +0000 Error: Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0., NSJSONSerializationErrorIndex=0} Retries: 1