For AI agents and LLMs: a structured documentation index is available at /llms.txt. Every page has a Markdown sibling — append .md to any URL.

Skip to content
Docs

Build a web checkout for your iOS app with the Web Monetization Kit

Step-by-step tutorial — deploy the Paddle Web Monetization Kit to Vercel, set up a product catalog, add a checkout button to your iOS app, and handle fulfillment using RevenueCat or webhooks.

AI summary

Deploy the Paddle Web Monetization Kit to Vercel, configure your iOS app's bundle ID and redirect URL, add a checkout button in Swift, and handle fulfillment via RevenueCat or webhooks to build an external purchase flow for your iOS app.

  • Set five env vars: APPLE_TEAM_ID, NEXT_PUBLIC_BUNDLE_IDENTIFIER, NEXT_PUBLIC_APP_REDIRECT_URL, NEXT_PUBLIC_PADDLE_CLIENT_TOKEN, NEXT_PUBLIC_PADDLE_ENV.
  • Pass price_id in the checkout URL — add app_user_id, user_email, and country_code for prefilled forms and RevenueCat entitlement matching.
  • After checkout, Paddle redirects to NEXT_PUBLIC_APP_REDIRECT_URL — handle the deeplink return in your app to verify the purchase and unlock features.

This tutorial walks through deploying the Paddle Web Monetization Kit to Vercel, setting up a product catalog, adding a checkout button to an iOS app, and handling fulfillment using either RevenueCat or webhooks. By the end, you'll have a complete external purchase flow for your iOS app.

For a quick overview of what's in the kit before you dive in, see the Web Monetization Kit reference.

Grid of logos used in the Web Monetization Kit: Paddle, RevenueCat, Next.js, and Vercel.

What's not covered

  • Authentication — we assume you already identify users via Firebase, Sign in with Apple, or similar.
  • Native in-app purchases — Paddle Checkout opens in Safari and redirects users back to your app. Like the App Store, it supports Apple Pay and other popular payment methods.
  • Subscription lifecycle management — pause, resume, cancel, update payment method. Use the prebuilt customer portal. Covered separately.

Before you begin

Sign up for Paddle

You can sign up for two kinds of account:

  • Sandbox — for testing and evaluation.
  • Live — for selling to customers.

We recommend a sandbox account for this tutorial. Sign up at sandbox-login.paddle.com/signup.

Sign up for Vercel and a Git provider

We deploy to Vercel, a serverless platform designed for Next.js. Sign up free if you don't already have an account.

You'll also need a Git provider. Vercel's deploy flow walks you through setting up GitHub (recommended), GitLab, or Bitbucket.

Prep your iOS development environment

Later, we add a button to your iOS app that opens the deployed checkout. You'll need:

  • Knowledge of iOS development, access to your project, and Xcode on macOS.
  • A URL scheme configured so Paddle Checkout can redirect users back to your app.

You don't need this to deploy the web checkout — come back to the iOS work later if a separate developer handles your iOS app.

Overview

Create and deploy a website checkout for your iOS app in six steps:

  1. Start deploy to Vercel
    Clone the repo, configure environment variables, and deploy.
  2. Set up your product catalog
    Create products and prices in Paddle, then update the app.
  3. Add your website
    Add the deploy URL to Paddle and get it approved.
  4. Add a checkout button to your app
    Open the checkout from your iOS app.
  5. Handle fulfillment and provisioning
    Use RevenueCat or process webhooks to fulfill purchases after a customer completes a checkout.
  6. Take a test payment
    Make a test purchase to make sure your purchase flow works correctly.

Start deploy to Vercel

To create a Vercel project ready for us to set up, click the button to get started:

Start one-click deploy to Vercel

Create Git repo

First, clone the starter kit repo. This creates a copy in your Git provider account so you can build on top of the project.

Click Continue with GitHub, Continue with GitLab, or Continue with Bitbucket to connect your Git provider, then enter a name for your repo.

Screenshot of the deploy to Vercel workflow, showing the Get started section. It shows three buttons to continue with GitHub, GitLab, and Bitbucket.

The repo name becomes the Vercel project name and is used for deploy preview URLs. If the name is taken, Vercel appends characters to your project name.

Configure environment variables

Five variables need to be set:

VariableDescription
APPLE_TEAM_IDYour Apple Developer team ID, required for universal links so you can link back to your app.
NEXT_PUBLIC_BUNDLE_IDENTIFIERYour iOS app's bundle ID, for example com.example.myapp.
NEXT_PUBLIC_APP_REDIRECT_URLCustom URL scheme that bounces users back to your app when their purchase completes, for example myapp://example-redirect.
NEXT_PUBLIC_PADDLE_CLIENT_TOKENClient-side token for securely opening Paddle Checkout.
NEXT_PUBLIC_PADDLE_ENVsandbox for sandbox accounts; production for live accounts.

Get Apple team ID and bundle ID

Paste each value as APPLE_TEAM_ID and NEXT_PUBLIC_BUNDLE_IDENTIFIER on the Vercel deploy screen.

Get a client-side token

Client-side tokens authenticate Paddle.js in your frontend.

  1. Go to Paddle > Developer tools > Authentication.
  2. Click the Client-side tokens tab, then New client-side token .
  3. Give it a name and description, then Save .
  4. Click next to the token, then Copy token .
  5. Paste it as NEXT_PUBLIC_PADDLE_CLIENT_TOKEN.

Paddle's Authentication dashboard showing the Client-side tokens tab with a list of tokens.

Set your environment

For NEXT_PUBLIC_PADDLE_ENV:

  • sandbox for a sandbox account.
  • production for a live account.

Review and deploy

Review your settings, then click Deploy. Wait for Vercel to build.

Screenshot of the complete screen for the deploy to Vercel workflow. It says 'congratulations!' and there's a preview of the app.

The deploy URL works and the marketing site renders. The pricing page and checkout flow won't work until the next step.

Set up your product catalog

Set up products and prices in Paddle to match the in-app items you want to sell.

Model your pricing

A complete product in Paddle has:

  • A product entity describing the item — name, description, image.
  • At least one related price entity describing how much and how often it's billed.

For this example, we create a product called Acme Guard with monthly and annual prices.

Illustration of the product catalog in Paddle. It shows a product called Acme Guard with monthly and annual prices.

Create products and prices

You can create products and prices using the Paddle dashboard or the API.

  1. Go to Paddle > Catalog > Products.
  2. Click New product .
  3. Enter details, then Save .
  4. Under Prices, click New price .
  5. Set the billing period to Monthly, then Save .
  6. Repeat for an Annually price.
  7. Click next to each price, then Copy ID .

Prices list with the action menu open and copy ID selected.

Update prices in your app

Clone your Git repo locally, then open src/components/pricing/plan-select.tsx in your IDE — or edit directly on your Git platform.

plan-select.tsx contains a plans array used in the pricing page. Swap each pri_ ID with one of yours. The tag is optional, useful for showing a discount label like Save 17%:

TypeScript
export const plans = [
{
priceId: "pri_01jx2rx1t30hxejpb5v0vav4nv", // Monthly price
},
{
priceId: "pri_01h1vjg3sqjj1y9tvazkdqe5vt", // Annual price
tag: "Save 17%",
}
];

Commit and push to main. Vercel rebuilds automatically.

Screenshot of the project page in Vercel showing a build in progress.

When the build completes, your pricing page displays the new prices. Checkout still doesn't work — that's the next step.

Add your website to Paddle

Before you can launch a checkout, add your Vercel deploy URL to Paddle.

Get your website approved

  1. Go to Paddle > Checkout > Website approval.
  2. Click Add a new domain, enter your Vercel deploy URL, then Submit for Approval.
  3. Wait for approval (instant on sandbox; a few days on live).

See website verification on the Paddle help center

Your default payment link is used to open Paddle Checkout for transactions and in emails that let customers manage purchases.

  1. Go to Paddle > Checkout > Checkout settings.
  2. Enter your Vercel deploy URL under Default payment link.
  3. Click Save .

Checkout settings screen with the default payment URL field highlighted.

Add a checkout button to your app

Add a button to your iOS app that:

  1. Checks whether in-app purchases are allowed on the device.
  2. Constructs a URL using your Vercel deploy URL with a price_id query parameter.
  3. Opens the URL in Safari.
PurchaseView.swift
import SwiftUI
import StoreKit // required for checking device payment capabilities using SKPaymentQueue
struct PurchaseView: View {
let checkoutBaseURL = "https://paddle-mobile-web-payments-starter.vercel.app" // replace with your checkout URL
let priceId = "pri_01h1vjg3sqjj1y9tvazkdqe5vt" // replace with a price ID or set dynamically
var body: some View {
VStack {
if SKPaymentQueue.canMakePayments() {
Button("Buy now") {
openCheckout()
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
} else {
Text("Purchases not available on this device.")
.foregroundColor(.secondary)
}
}
.padding()
}
func openCheckout() {
let checkoutURL = "\(checkoutBaseURL)?price_id=\(priceId)"
if let url = URL(string: checkoutURL) {
UIApplication.shared.open(url)
}
}
}

Prefill information Recommended

Pass URL query parameters to prefill checkout fields and provide a smoother user experience. For example, pass the customer email and a unique identifier for use with RevenueCat:

PurchaseView.swift
import SwiftUI
import StoreKit
struct PurchaseView: View {
let checkoutBaseURL = "https://paddle-mobile-web-payments-starter.vercel.app"
let priceId = "pri_01h1vjg3sqjj1y9tvazkdqe5vt"
// From your auth platform / RevenueCat
let appUserId = "85886aac-eef6-41df-8133-743cbb1daa4b"
let userEmail = "sam@example.com"
let countryCode = "US"
let postalCode = "10021"
var body: some View {
VStack {
if SKPaymentQueue.canMakePayments() {
Button("Buy now") {
openCheckout()
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
} else {
Text("Purchases not available")
.foregroundColor(.secondary)
}
}
.padding()
}
func openCheckout() {
var urlComponents = URLComponents(string: checkoutBaseURL)!
urlComponents.queryItems = [
URLQueryItem(name: "price_id", value: priceId),
URLQueryItem(name: "app_user_id", value: appUserId),
URLQueryItem(name: "user_email", value: userEmail),
URLQueryItem(name: "country_code", value: countryCode),
URLQueryItem(name: "postal_code", value: postalCode)
]
if let url = urlComponents.url {
UIApplication.shared.open(url)
}
}
}

For the full list of supported parameters, see hosted checkout URL parameters.

Handle fulfillment and provisioning

After a customer completes a purchase, Paddle redirects them back to your app. Now you need to unlock the features they bought. Two options:

If you use the RevenueCat x Paddle integration for entitlements, you're set:

  1. Paddle sends data to RevenueCat about the completed checkout.
  2. RevenueCat grants the user an entitlement based on your product configuration.
  3. Use the RevenueCat SDK to check entitlement status in your iOS app.

If you'd rather build your own fulfillment, use webhooks. The example below grants users access when they purchase the Acme Guard product.

Build a webhook handler

This Node.js example uses Express. Adapt to your framework — see the webhook signature verification guide for examples in other languages.

server.js
app.post("/paddle/webhooks", express.raw({ type: 'application/json' }), async (req, res) => {
try {
// Verify the webhook signature first — see /webhooks/verify-signatures
const payload = JSON.parse(req.body.toString());
const { data, event_type } = payload;
const occurredAt = payload.occurred_at;
switch (event_type) {
case 'transaction.created': {
const userForTransaction = await User.findOne({ where: { paddleCustomerId: data.customer_id } });
if (userForTransaction) {
await Transaction.create({
transactionId: data.id,
userId: userForTransaction.id,
subscriptionId: data.subscription_id,
status: data.status,
amount: data.amount,
currencyCode: data.currency_code,
occurredAt,
});
}
break;
}
case 'transaction.completed': {
const completedTransaction = await Transaction.findOne({ where: { transactionId: data.id } });
if (completedTransaction) {
await completedTransaction.update({
status: data.status,
subscriptionId: data.subscription_id,
invoiceId: data.invoice_id,
invoiceNumber: data.invoice_number,
billedAt: data.billed_at,
updatedAt: data.updated_at,
});
}
break;
}
}
res.json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
return res.status(500).json({ error: error.message });
}
});

Unlock user access

When you receive transaction.completed, update the user's access permissions. The example below maps Paddle product IDs to permission keys; your iOS app reads the permission to unlock features.

server.js
case 'transaction.completed': {
const completedTransaction = await Transaction.findOne({ where: { transactionId: data.id } });
if (completedTransaction) {
await completedTransaction.update({
status: data.status,
subscriptionId: data.subscription_id,
invoiceId: data.invoice_id,
invoiceNumber: data.invoice_number,
billedAt: data.billed_at,
updatedAt: data.updated_at,
});
const user = await User.findOne({ where: { id: completedTransaction.userId } });
if (!user) break;
const purchasedItems = data.items || [];
const accessPermissions = user.accessPermissions
? JSON.parse(user.accessPermissions)
: {};
// Map product IDs to permission keys.
const productToPermission = {
'pro_01j4z97mq9pa4fkyy0wqenepkz': 'acmeGuardAccess',
'pro_01j4vjes1y163xfj1rh1tkfb65': 'acmeHotspotAccess',
};
purchasedItems.forEach(({ price }) => {
const permissionKey = productToPermission[price.product_id];
if (permissionKey) accessPermissions[permissionKey] = true;
});
await user.update({
accessPermissions: JSON.stringify(accessPermissions),
});
}
break;
}

Create a notification destination

Tell Paddle where to deliver webhooks:

  1. Go to Paddle > Developer tools > Notifications.
  2. Click New destination.
  3. Give it a name.
  4. Keep notification type as webhook (the default).
  5. Enter your webhook handler URL, then check transaction.completed.
  6. Click Save destination.

Illustration of the new destination drawer in Paddle. It shows fields for description, type, URL, and version. Under those fields, there's a section called events with a checkbox that says 'select all events'

See create a notification destination for more.

Test the complete flow

Mobile app screen showing a 'Buy now for $60.00' button.

Paddle checkout screen with product details and Apple Pay option.

Mobile app screen showing 'You unlocked access for 1 year. Enjoy!' with a 'Start now' button.

Run through the purchase flow end-to-end. Use test card details for sandbox:

FieldValue
Email addressAn email address you own
CountryAny valid country supported by Paddle
ZIP code (if required)Any valid ZIP or postal code
Card number4242 4242 4242 4242
Name on cardAny name
Expiration dateAny valid date in the future
Security code100

Next steps

That's it. Now you've built a website checkout for your iOS app, you might like to hook into other features of the Paddle platform.

Customize the checkout

Our tutorial set up an inline checkout where customers purchase your iOS products. When a customer interacts with the checkout, Paddle.js sends events that are used to show and update the order summary that customers see.

While the checkout flow is already set up to function with all Paddle prices and currencies out-of-the-box, you might like to customize its appearance and change its behavior.

Customize the inline checkout form

Paddle.js initializes and opens the checkout in src/hooks/use-paddle.tsx. Edit the checkout.settings parameter on either method to customize appearance or remove fields. See Paddle.Initialize() for the full list of settings.

Customize the order summary

product-details.tsx and order-summary.tsx in src/components/checkout hold the order summary above the inline checkout. Use the checkoutData object to access Paddle.js event fields.

Pass a discount

Extend your pricing page and checkout flow by passing a discount to reduce the amount a customer pays.

  • For your pricing page, add discountId in your request to Paddle.PricePreview(). The response includes a discount array that has information about the discount applied. Calculated totals in details.lineItems include discounts, where applicable.
  • For the checkout flow, add discount_id as a query parameter in the URL you use to open Paddle Checkout within your iOS app. The product summary shows the discount automatically, using totals.discount from the checkout event.

Learn more about Paddle

When you use Paddle, we take care of payments, tax, subscriptions, and metrics with one unified platform. Customers can self-serve with the portal, and Paddle handles any order inquiries for you.

Payment methods

Accept global payment methods out of the box, including credit/debit cards, Apple Pay, and more.

Metrics and reports

Get revenue metrics, customer analytics, and detailed sales reports in your dashboard.

Customer portal

Allow customers to manage invoices, subscriptions, and account details without your intervention.

Was this page helpful?