Skip to content
Create account or Sign in
The Stripe Docs logo
/
Ask AI
Create accountSign in
Get started
Payments
Revenue
Platforms and marketplaces
Money management
Developer resources
APIs & SDKsHelp
OverviewAccept a paymentUpgrade your integration
Online payments
OverviewFind your use case
Use Payment Links
Use a prebuilt checkout page
Build a custom integration with Elements
Build an in-app integration
Use Managed Payments
    Overview
    How Managed Payments works
    Changelog
    Get started
    Set up Managed Payments
    Update a Checkout integration
    Mobile app payments
Recurring payments
In-person payments
Terminal
Payment methods
Add payment methods
Manage payment methods
Faster checkout with Link
Payment operations
Analytics
Balances and settlement time
Compliance and security
Currencies
Declines
Disputes
Fraud prevention
Radar fraud protection
Payouts
ReceiptsRefunds and cancellations
Advanced integrations
Custom payment flows
Flexible acquiring
Off-Session Payments
Multiprocessor orchestration
Beyond payments
Incorporate your company
Crypto
Agentic commerce
Machine payments
Financial Connections
Climate
Verify identities
United States
English (United States)
HomePaymentsUse Managed Payments

Accept payments for digital products on iOS with Stripe as your merchant of recordPublic preview

Open Stripe Checkout with Managed Payments in a browser to sell in-app digital products or subscriptions.

Terms of service required

You must accept the Managed Payments terms of service in the Dashboard before you can use Managed Payments.

In some countries, you can link to an external website to accept payments using Managed Payments on iOS. You use Stripe Checkout to redirect your customers to a Stripe-hosted payment page. As an example, this guide describes how to sell digital credits for use in your app. You can accept both one-time or subscription payments.

One-time payment

The UI customers see for one-time payments with Managed Payments

Recurring payment

The UI customers see for subscription payments with Managed Payments

Limitations

This guide doesn’t cover:

  • 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.

This guide only describes the process for selling in-app digital products that follow this eligibility criteria. If your digital products don’t match this criteria, see Accept payments for digital goods on iOS. If you sell physical products, see Stripe in-app payments.

Before you begin

  • Make sure your products meet the eligibility requirements for Managed Payments. To process a payment with Managed Payments, all of the products the customer is purchasing must be eligible.
  • Activate Managed Payments in your Dashboard.
  • Set up your development environment.
  • Make sure you’re using API version 2025-03-31.basil or higher.

Create products and prices

Create your products and their prices in the Dashboard or with the Stripe CLI. You can add digital products with one-off prices and subscriptions using recurring prices. You can also let your customer pay what they want (for example, to decide how many credits to buy), by selecting Customers choose what to pay. When you create your Product, the tax code you select must be eligible for Managed Payments. Eligible tax codes are labeled “Eligible for Managed Payments”.

This example uses a single product and price to represent a 100 coin bundle that costs 10 USD.

Go to the Add a product page and create the coin bundle. Add a one-time price of 10 USD. Select txcd_10201000 for the tax code Video Games - downloaded - non subscription - with permanent rights tax code.

  • 100 coins: Bundle of 100 in-app coins
  • Price: Standard model | 10 USD | One-time | txcd_10201000

After you create the price, record the price ID so you can use it in subsequent steps. Price IDs look like this: price_G0FvDp6vZvdwRZ.

When you’re ready, click Copy to live mode to clone your product from a testing environment to live mode.

Create customers
Server-side

Each time you create a Checkout session, create a Customer object for your user if one doesn’t already exist.

Node.js
No results
// Set your secret key. Remember to switch to your live secret key in production. // See your keys here: https://dashboard.stripe.com/apikeys const stripe = require('stripe')(
'sk_test_BQokikJOvBiI2HlWgH4olfQ2'
); // This assumes your app has an existing user database, which we'll call `myUserDB`. const user = myUserDB.getUser("jennyrosen"); if (!user.stripeCustomerID) { const customer = await stripe.customers.create({ name: user.name, email: user.email, }); // Set the user's Stripe Customer ID for later retrieval. user.stripeCustomerID = customer.id; }

Warning

Make sure to 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 your app doesn’t have an existing authentication provider, you can use Sign in with Apple.

Use the customer argument to pass their Customer ID when creating a Checkout Session. This ensures that all objects created during the session associate with the correct Customer object.

Set up universal links
Client-sideServer-side

Universal links allow Checkout 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 Checkout 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 MIME type application/json. Use curl -I to confirm the content type.

Command Line
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

  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 success_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.

Create a Checkout session
Server-side

A Checkout Session is the programmatic representation of what your customer sees when they’re redirected to the payment form. Checkout Sessions expire 24 hours after creation. Configure it with:

  • The customer ID
  • The product ID (either a one-time payment or subscription)
  • An origin_context set to mobile_app to opt in to a UI that’s optimized for app-to-web purchases.
  • The managed_payments[enabled] parameter to true
  • The version header with ;managed_payments_preview=v1
  • A success_url, a universal link to redirect your customer to your app after they complete the payment.

Common mistake

Create the Checkout Session with managed_payments: {enabled: true}, version header with managed_payments_preview=v1 and origin_context: "mobile_app" to opt in to a Managed Payments specific UI that’s optimized for app-to-web purchases.

If you’re using TypeScript, you might see type errors when doing this because Managed Payments is in private preview. You can safely ignore these errors by adding // @ts-expect-error.

After creating a Checkout Session, return the URL from the response to your app.

Node.js
No results
// This example sets up an endpoint using the Express framework. const express = require('express'); const app = express(); const stripe = require('stripe')(
'sk_test_BQokikJOvBiI2HlWgH4olfQ2'
, { apiVersion: '2025-08-27.basil', stripeAccount: 'managed_payments_preview=v1' }); app.post('/create-checkout-session', async (req, res) => { // Fetch the Stripe customer ID for the customer associated with this request. // This assumes your app has an existing user database, which we'll call `myUserDB`. const user = myUserDB.getUserFromToken(req.query.token); const customerId = user.stripeCustomerID; // The price ID from the previous step const priceId = '{{PRICE_ID}}'; const session = await stripe.checkout.sessions.create({ line_items: [ { price: priceId, quantity: 1, }, ], mode: 'payment', managed_payments: {enabled: true}, origin_context: 'mobile_app', customer: customerId, success_url: 'https://example.com/checkout_redirect/success', }); res.json({url: session.url}); }); app.post('/login', async (req, res) => { // This assumes your app has an existing user database, which we'll call `myUserDB`. const token = myUserDB.login(req.body.login_details) res.json({token: token}) }); app.listen(4242, () => console.log(`Listening on port ${4242}!`));

Open Checkout in Safari
Client-side

Add a checkout button to your app. This button:

  1. Calls your server-side endpoint to create a Checkout session.
  2. Returns the Checkout session to the client.
  3. Opens the session URL in Safari.
CheckoutView.swift
import Foundation import SwiftUI import StoreKit struct BuyCoinsView: View { @EnvironmentObject var myBackend: MyServer @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 { myBackend.createCheckoutSession { url in UIApplication.shared.open(url, options: [:], completionHandler: nil) } } label: { Text("Buy 100 coins") }.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 } } } } } }

Fetch the Checkout URL on the client

Use your server endpoint to fetch the checkout session.

CheckoutView.swift
class MyServer: ObservableObject { // The cached login token var token: String? func createCheckoutSession(completion: @escaping (URL) -> Void) { // Send the login token to the `/create_checkout_session` endpoint let request = URLRequest(url: URL(string: "https://example.com/create-checkout-session?token=\(self.token)")!) let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in guard let unwrappedData = data, let json = try? JSONSerialization.jsonObject(with: unwrappedData, options: []) as? [String : Any], let urlString = json["url"] as? String, let url = URL(string: urlString) else { // Handle error return } DispatchQueue.main.async { // Call the completion block with the Checkout session URL returned from the backend completion(url) } }) task.resume() } func login() { // Login using the server and set the login token. let request = URLRequest(url: URL(string: "https://example.com/login")!) let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in guard let unwrappedData = data, let json = try? JSONSerialization.jsonObject(with: unwrappedData, options: []) as? [String : Any], let token = json["token"] as? String else { // Handle error return } self.token = token }) task.resume() } }

Handle order fulfillment
Server-side

After the purchase succeeds, Stripe sends you a checkout.session.completed webhook. When you receive this event, you can add the coins to the customer on your server.

Checkout redirects your customer to the success_url when you acknowledge you received the event. In scenarios where your endpoint is down or the event isn’t acknowledged properly, Checkout redirects the customer to the success_url 10 seconds after a successful payment.

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 STRIPE_WEBHOOK_SECRET key, click the webhook in the Dashboard to view it.

server.js
Node.js
No results
// Set your secret key. Remember to switch to your live secret key in production. // See your keys here: https://dashboard.stripe.com/apikeys const stripe = require('stripe')(
'sk_test_BQokikJOvBiI2HlWgH4olfQ2'
); 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 'checkout.session.completed': const session = event.data.object; // Payment is successful. // Update the customer in your database to reflect this purchase. const user = myUserDB.userForStripeCustomerID(session.customer); user.addCoinsTransaction(100, session.id); break; default: // Unhandled event type } res.sendStatus(200); });

Testing

Test your checkout button that redirects your customer to Stripe Checkout.

  1. Click the checkout button, which redirects you to the Stripe Checkout payment form.
  2. Enter the test number , a three-digit CVC, a future expiration date, and any valid postal code.
  3. Tap Pay.
  4. The checkout.session.completed webhook fires, and Stripe notifies your server about the transaction.
  5. You’re redirected back to your app.

If your integration isn’t working, see the additional testing resources section below.

Payment details

  1. After you confirm the test payment, go to the Dashboard > Payments list
  2. Click your test payment to view the payment details. This page shows the:
    • Product that was purchased
    • Subscription that was created
    • Invoice that was created
    • Amount of tax calculated and withheld through Managed Payments
    • Statement descriptor that displays on your customer’s statements

Customer authorization

The payment method attached to the subscription is only authorized to be charged by Managed Payments. To use it for non-Managed-Payments transactions, you must obtain customer authorization.

Preview the receipt

  1. Under Checkout summary, click Invoice.
  2. Click Send receipt to preview the receipt email sent to your customer. You can also download the receipt.

Note

In the sandbox, you don’t receive email receipts automatically after a purchase. Send them manually using the instructions in the preceding section.

Link

Link acts as the merchant of record at checkout and provides subscription management and transaction support at Link.com.

To test Link:

  1. Open your checkout page
  2. Click the checkout button.
  3. Enter the same email address you used to test your checkout page.
  4. In the pop-up modal, use the test passcode 000000 to authenticate.

If you selected the Save my information for faster checkout checkbox during the first checkout, you also see the 4242 test card saved to your Link account.

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.

NumberDescription
Succeeds and immediately processes the payment.
Complete 3D Secure 2 authentication for a successful payment.
Always fails with a decline code of insufficient_funds.

For the full list of test cards see the testing guide.

Testing universal links

If your universal link doesn’t redirect from Checkout to your app, check the SharedWebCredentials logs for errors.

  1. 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)
  2. 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.
  3. Delete and reinstall your app. This causes iOS to re-fetch the apple-app-site-association file.

  4. Complete the checkout flow in your app.

  5. Checkout redirects you to your app. If it doesn’t, take a sysdiagnose.

  6. 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.

  7. Wait 5 minutes, then go to Settings > Privacy > Analytics & Improvement > Analytics Data, and scroll to the last sysdiagnose file in the list.

  8. Tap the share button to AirDrop the file to your computer.

  9. Open the sysdiagnose archive, then open swcutil_show.txt

  10. 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

See also

  • Add discounts
  • Customize your branding
  • Customize your success page
Was this page helpful?
YesNo
  • Need help? Contact Support.
  • Check out our changelog.
  • Questions? Contact Sales.
  • LLM? Read llms.txt.
  • Powered by Markdoc