> For the complete documentation index, see [llms.txt](https://developer.paddle.com/llms.txt).

# Paddle for mobile

Use Paddle's web checkout from iOS and Android apps — link out, take payment, and return via deeplink. Paddle handles tax, fraud, and global payments outside the app stores.

---

Paddle isn't a native in-app purchase SDK. It's a merchant-of-record alternative to Apple StoreKit and Google Play Billing — your mobile app links out to a Paddle-powered web checkout, the customer pays with Apple Pay, Google Pay, cards, PayPal, or local methods, and returns to your app via deeplink. Paddle handles tax, fraud, subscriptions, and receipts.

What Paddle does **not** do: native StoreKit or Play Billing integrations, on-device receipt validation, or in-app purchases for content categories Apple and Google still mandate native billing for.

{% callout type="note" %}
Already on RevenueCat? Paddle plugs into the [RevenueCat x Paddle integration](https://www.paddle.com/revenuecat-integration-beta) for entitlement management. See [Grant entitlements](#grant-entitlements) below.
{% /callout %}

## The link-out architecture

Every Paddle mobile flow follows the same shape:

1. Your app opens a URL — either a Paddle-hosted checkout link or a page on your own website that loads [Paddle.js](https://developer.paddle.com/paddle-js/include-paddlejs.md).
2. The customer pays in their browser. Paddle calculates tax, runs fraud checks, and routes the payment.
3. When payment completes, the browser redirects back to your app using a custom URL scheme or [universal link](https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content/).
4. Your app verifies the purchase against your backend or RevenueCat, then unlocks the feature.

The work splits cleanly: **the web side handles checkout** (UI, payment processing, tax), **your backend handles entitlement** (webhooks, your database), and **the app side handles two small things** — opening a URL and listening for the return deeplink.

## Choose your path

Pick the integration based on how much of the web checkout you want to host yourself.

{% card-group cols=3 %}
{% card title="Hosted checkout" icon="carbon:cloud" %}
Paddle hosts the entire checkout. You configure a checkout link in the dashboard and open it from your app. Lowest setup. Requires Paddle approval on live accounts.
{% /card %}
{% card title="Web Monetization Kit" icon="carbon:rocket" %}
Recommended default. A Next.js app you deploy to Vercel — includes pricing, marketing, and legal pages so you can pass website approval. Tightly integrated with Paddle.js.
{% /card %}
{% card title="Custom workflow" icon="carbon:code" %}
Your own website with Paddle.js, your own backend creating transactions. Most control. Works when you already have a web property you want to extend.
{% /card %}
{% /card-group %}

The Web Monetization Kit is the recommended starting point for most teams — see [the starter kit tutorial](https://developer.paddle.com/get-started/starter-kits/web-monetization.md). For full hosted checkout setup, see [Add a hosted checkout to your mobile app](https://developer.paddle.com/build/mobile-apps/link-out-mobile-app-hosted-checkout-app.md). For a custom Paddle.js implementation, see [Build a custom mobile checkout](https://developer.paddle.com/build/mobile-apps/link-out-mobile-app-custom-workflow.md).

## App-side: open checkout and handle return

Whichever path you pick, the app-side code does two things: open a URL and respond when the operating system hands control back. The recipes below cover the platform-specific mechanics.

{% callout type="info" %}
The deeplink payload isn't trusted. Always verify the purchase against your backend (or RevenueCat) before unlocking access. The deeplink is a signal that the user has returned, not proof that payment completed.
{% /callout %}

{% tabs %}
{% tab-item title="iOS (SwiftUI)" %}

Open the checkout URL using `UIApplication.shared.open()`. Handle the return in your scene's root view with `.onOpenURL`. Make sure your URL scheme is [registered in Xcode](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app).

```swift title="ContentView.swift"
import SwiftUI

struct ContentView: View {
    var body: some View {
        Button("Buy now") {
            let url = URL(string: "https://pay.paddle.io/checkout/hsc_...?price_id=pri_...")!
            UIApplication.shared.open(url)
        }
        .onOpenURL { incoming in
            let components = URLComponents(url: incoming, resolvingAgainstBaseURL: true)
            let transactionId = components?.queryItems?
                .first(where: { $0.name == "transactionId" })?.value
            Task { await refreshEntitlement(transactionId: transactionId) }
        }
    }
}
```

{% /tab-item %}
{% tab-item title="Android (Kotlin)" %}

Open the URL with an `ACTION_VIEW` intent. Receive the return URL via `onNewIntent` after configuring an [App Link or intent filter](https://developer.android.com/training/app-links).

<!-- TODO: verify against working app -->

```kotlin title="MainActivity.kt"
import android.content.Intent
import android.net.Uri
import androidx.activity.ComponentActivity

class MainActivity : ComponentActivity() {
    fun openCheckout() {
        val url = Uri.parse("https://pay.paddle.io/checkout/hsc_...?price_id=pri_...")
        startActivity(Intent(Intent.ACTION_VIEW, url))
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        val transactionId = intent.data?.getQueryParameter("transactionId")
        lifecycleScope.launch { refreshEntitlement(transactionId) }
    }
}
```

{% /tab-item %}
{% tab-item title="React Native" %}

Use `Linking` from `react-native` to open the URL and listen for incoming deeplinks.

<!-- TODO: verify against working app -->

```typescript title="PurchaseScreen.tsx"
import { useEffect } from 'react';
import { Button, Linking } from 'react-native';

export function PurchaseScreen() {
  useEffect(() => {
    const subscription = Linking.addEventListener('url', ({ url }) => {
      const transactionId = new URL(url).searchParams.get('transactionId');
      refreshEntitlement(transactionId);
    });
    return () => subscription.remove();
  }, []);

  return (
    <Button
      title="Buy now"
      onPress={() => Linking.openURL('https://pay.paddle.io/checkout/hsc_...?price_id=pri_...')}
    />
  );
}
```

{% /tab-item %}
{% tab-item title="Expo" %}

Use `expo-linking` for both opening the URL and receiving the return. The `useURL` hook fires whenever the app is opened by a deeplink.

<!-- TODO: verify against working app -->

```typescript title="PurchaseScreen.tsx"
import { useEffect } from 'react';
import { Button } from 'react-native';
import * as Linking from 'expo-linking';
import { useURL } from 'expo-linking';

export function PurchaseScreen() {
  const url = useURL();

  useEffect(() => {
    if (!url) return;
    const { queryParams } = Linking.parse(url);
    refreshEntitlement(queryParams?.transactionId as string);
  }, [url]);

  return (
    <Button
      title="Buy now"
      onPress={() =>
        Linking.openURL('https://pay.paddle.io/checkout/hsc_...?price_id=pri_...')
      }
    />
  );
}
```

{% /tab-item %}
{% tab-item title="Flutter" %}

Open the URL with `url_launcher`. Listen for incoming links with `app_links`.

<!-- TODO: verify against working app -->

```dart title="purchase_page.dart"
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

class PurchasePage extends StatefulWidget {
  @override
  State<PurchasePage> createState() => _PurchasePageState();
}

class _PurchasePageState extends State<PurchasePage> {
  final _appLinks = AppLinks();

  @override
  void initState() {
    super.initState();
    _appLinks.uriLinkStream.listen((uri) {
      final transactionId = uri.queryParameters['transactionId'];
      refreshEntitlement(transactionId);
    });
  }

  @override
  Widget build(BuildContext context) => ElevatedButton(
        onPressed: () => launchUrl(
          Uri.parse('https://pay.paddle.io/checkout/hsc_...?price_id=pri_...'),
        ),
        child: const Text('Buy now'),
      );
}
```

{% /tab-item %}
{% /tabs %}

## Identity across the boundary

Mobile users are signed into your app, but the browser they open for checkout is anonymous. Pass identity through URL parameters so Paddle can associate the purchase with the right account.

| Parameter      | Purpose                                                              |
| -------------- | -------------------------------------------------------------------- |
| `app_user_id`  | Your internal user ID. Useful for RevenueCat or your own backend.    |
| `user_email`   | Pre-fill the customer email field at checkout.                       |
| `country_code` | Two-letter ISO code. Localizes pricing and tax.                      |
| `postal_code`  | Pre-fill ZIP/postal code where Paddle requires it.                   |

For the full list, see [Hosted checkout URL parameters](https://developer.paddle.com/paddle-js/hosted-checkout.md).

When the customer returns via deeplink, include a `transactionId` query parameter so your app can match the return to the original checkout. The Paddle.js [`checkout.completed`](https://developer.paddle.com/paddle-js/events/checkout-completed.md) event exposes the transaction ID, which you append to your redirect URL.

## Grant entitlements

Webhooks are the source of truth for entitlement — never trust the deeplink alone. Two patterns:

### Using Paddle webhooks and your backend

Your backend listens for [`transaction.completed`](https://developer.paddle.com/webhooks/transactions/transaction-completed.md) (and the subscription lifecycle events if you're selling subscriptions), updates your database, and exposes an entitlement endpoint your app calls on resume. This is the canonical pattern — see [Provision access with webhooks](https://developer.paddle.com/build/subscriptions/provision-access-webhooks.md).

```text
1. App opens checkout URL with app_user_id={your-user-id}
2. Customer pays. Paddle fires transaction.completed.
3. Your webhook handler matches the customer to your user, updates entitlements.
4. App resumes via deeplink. App calls your /entitlements endpoint.
5. Your backend returns the current entitlement state. App unlocks features.
```

### Using RevenueCat

If your team already manages entitlements through RevenueCat, the [RevenueCat x Paddle integration](https://www.paddle.com/revenuecat-integration-beta) syncs Paddle purchases automatically. You pass `app_user_id` (or a custom RevenueCat ID) through the checkout URL, and RevenueCat receives the entitlement event from Paddle. Your app uses the standard RevenueCat SDK to check entitlements on resume.

The trade-off: RevenueCat owns the entitlement model, which is a good fit if you already have iOS apps using StoreKit alongside web checkout. If Paddle is your only billing platform, the webhook pattern keeps the entire flow on your infrastructure.

## Regulatory context

App store rules around external purchase links are still evolving — pricing your business on a specific commission rate isn't safe in 2026.

Following the December 2025 Ninth Circuit ruling in *Epic Games v. Apple*, Apple may charge a commission on linked-out purchases. The court overturned the lower court's complete ban on commissions but found the original 27% rate too high; the specific rate is being determined by the district court. EU markets follow a separate fee structure under the Digital Markets Act, which Apple is implementing through its Clearly Trackable Costs (CTC) model.

Plan for fees that may shift, and design your unit economics with headroom. Paddle's web checkout still gives you direct payment relationships, full pricing control, and global payment methods that the app stores can't match — but treat any commission claim with a date attached.

<!-- TODO: link to /concepts/regulations/mobile-app-stores once it exists -->

For the current state of compliance and approval requirements on the Paddle side, see [Hosted checkout for mobile apps](https://developer.paddle.com/concepts/sell/hosted-checkout-mobile-apps.md).

## Where to start

{% card-group cols=3 %}
{% card title="Web Monetization Kit" icon="carbon:rocket" url="/get-started/starter-kits/web-monetization" %}
Deploy the recommended Next.js + Vercel starter kit, including all the pages you need for website approval.
{% /card %}
{% card title="Hosted checkout (iOS)" icon="carbon:cloud" url="/build/mobile-apps/link-out-mobile-app-hosted-checkout-app" %}
Step-by-step iOS tutorial — link out to a Paddle-hosted checkout, handle webhooks, take a test payment.
{% /card %}
{% card title="Custom workflow (iOS)" icon="carbon:code" url="/build/mobile-apps/link-out-mobile-app-custom-workflow" %}
Build your own checkout page with Paddle.js, then link out from your iOS app.
{% /card %}
{% /card-group %}