Skip to content
Create account
or
Sign in
The Stripe Docs logo
/
Ask AI
Create account
Sign in
Get started
Payments
Finance automation
Platforms and marketplaces
Money management
Developer tools
Get started
Payments
Finance automation
Get started
Payments
Finance automation
Platforms and marketplaces
Money management
Overview
About Stripe payments
Upgrade your integration
Payments analytics
Online payments
OverviewFind your use caseManaged Payments
Use Payment Links
Build a checkout page
Build an advanced integration
Build an in-app integration
    Overview
    Payment Sheet
    Embedded Payment Element
    Link out for in-app purchases
      Use a prebuilt payment page
      Use Payment Links (low-code)
      Build a custom flow
    Collect addresses
    US and Canadian cards
Payment methods
Add payment methods
Manage payment methods
Faster checkout with Link
Payment interfaces
Payment Links
Checkout
Web Elements
In-app Elements
Payment scenarios
Custom payment flows
Flexible acquiring
Orchestration
In-person payments
Terminal
Other Stripe products
Financial Connections
Crypto
Climate
HomePaymentsBuild an in-app integrationLink out for in-app purchases

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.

Copy page

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 Stripe
Server-side

First, register for a Stripe account.

Then add the Stripe API library to your backend:

Command Line
Ruby
# Available as a gem sudo gem install stripe
Gemfile
Ruby
# If you use bundler, you can add this line to your Gemfile gem 'stripe'

Next, install the Stripe CLI. The CLI provides the required webhook testing, and you can run it to create your products and prices.

Command Line
homebrew
# Install Homebrew to run this command: https://brew.sh/ brew install stripe/stripe-cli/stripe # Connect the CLI to your dashboard stripe login

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

Next, click Copy to live mode to clone your product from a testing environment to live mode.

Create customers
Server-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.
Node
// 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

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 Subscription
Server-side

When creating a subscription to use the Payment Element, you typically pass payment_behavior: 'default_incomplete'. This tells Stripe to create a subscription in incomplete status and generate a Payment Intent for the initial payment.

Note

Store the subscription.id in your database to manage future subscription events such as cancellation, upgrades, and downgrades.

Node
// This example sets up an endpoint using the Express framework. // Watch this video to get started: https://youtu.be/rPR2aJ6XnAc. const express = require('express'); const app = express(); const stripe = require('stripe')(
'sk_test_BQokikJOvBiI2HlWgH4olfQ2'
) app.post('/create-subscription', async (req, res) => { const { priceId, customerId } = req.body; // Create the subscription // setting payment_behavior to "default_incomplete" ensures we get a Payment Intent // that we can confirm on the client using the Payment Element const subscription = await stripe.subscriptions.create({ customer: customerId, items: [{ price: priceId }], payment_behavior: 'default_incomplete', expand: ['latest_invoice.payment_intent'], }); // Make sure you associate the subscription ID with the user in your database! myUserDB.addUserSubscription("jennyrosen", subscription.id); // Get the Payment Intent client secret const paymentIntent = subscription.latest_invoice.payment_intent; const clientSecret = paymentIntent.client_secret; return res.json({ subscriptionId: subscription.id, clientSecret: clientSecret, }); }); 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}!`));

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_method_types 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 .well-known/apple-app-site-association 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.

.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" } ] } ] } }

Warning

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:yourdomain.com to the Associated Domains list.

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_redirect/success page and a /checkout_redirect/cancel page.

Open Checkout in Safari
Client-side

Add a checkout button to your app. This button opens your custom checkout page in Safari.

CheckoutView.swift
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 app
Server-side

With Elements, make sure you redirect users back to your app (using the registered universal link) on a successful payment confirmation.

Item 1
stripe.confirmPayment({ elements, confirmParams: { // Return URL where the customer should be redirected after the PaymentIntent is confirmed. return_url: 'https://example.com/checkout_redirect/success', }, }) .then(function(result) { if (result.error) { // Inform the customer that there was an error. } });

Handle order fulfillment
Server-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:

Node
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { console.error('Webhook signature verification failed.', err.message); return res.sendStatus(400); } switch (event.type) { case 'invoice.payment_succeeded': { const invoice = event.data.object; // Mark subscription as active in your database // e.g., invoice.subscription -> "sub_abc123" console.log('Payment succeeded'); break; } case 'invoice.payment_failed': { const invoice = event.data.object; console.log('Payment failed - notify the user to update their payment methods'); break; } case 'customer.subscription.updated': { const subscription = event.data.object; // e.g., handle pause, cancellation, or other changes console.log(`Subscription updated: ${subscription.id}`); break; } default: console.log(`Unhandled event type ${event.type}`); } res.json({ received: true }); });

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

Testing

To test your that your checkout button works, do the following:

  1. Click the checkout button, which redirects you to your checkout with Stripe’s Payment Element.
  2. Enter the test number , a three-digit CVC, a future expiration date, and any valid postal code.
  3. Tap Pay.
  4. The invoice.payment_succeeded 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 additional testing resources.

OptionalAdditional testing resources

See also

  • Add discounts
  • Collect taxes
  • Collect tax IDs
  • Customize your branding
  • Customize your success page
Was this page helpful?
YesNo
Need help? Contact Support.
Join our early access program.
Check out our changelog.
Questions? Contact Sales.
LLM? Read llms.txt.
Powered by Markdoc