> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile WebView Integration

> Render the embedded checkout inside your own WebView on iOS, Android, or React Native

The embedded checkout renders from a URL, so any mobile app can display it in a WebView: create an order server-side, build the checkout URL, and load it. This guide covers the direct WebView integration for apps that do not use a Crossmint client SDK, including native iOS (Swift) and Android (Kotlin) apps.

<Note>
  The [React Native](/payments/embedded/quickstarts/credit-card-memecoin-react-native),
  [Flutter](/payments/embedded/quickstarts/credit-card-memecoin-flutter), and
  [Kotlin](/sdk-reference/checkout/kotlin/index) SDKs render and configure this WebView for you. If you build with one
  of those frameworks, start from the SDK instead and return here only if you need full control over the WebView.
</Note>

## Prerequisites

* **API keys**: From the [Crossmint Console](https://staging.crossmint.com/console), under **Integrate → API Keys**, create a **server key** (`sk_staging_...`) with the `orders.create` and `orders.read` scopes, and a **client key** (`ck_staging_...`) with the `orders.read` scope.
* **A recipient wallet**: purchased assets are delivered to a wallet address that you pass at order creation.
* **A backend**: order creation uses the server key, which must never ship inside the app.

## Integration

<Steps>
  <Step title="Create the order server-side">
    Create the order from your backend and return the `orderId` and `clientSecret` to the app. The order carries the item, the recipient, and optionally the receipt email, so the checkout never needs to ask the user for them:

    ```javascript theme={null}
    // server.js
    const CROSSMINT_SERVER_API_KEY = "YOUR_SERVER_API_KEY"; // sk_staging_...

    // sk_staging_ keys pair with staging.crossmint.com,
    // sk_production_ keys with www.crossmint.com
    const BASE_URL = "https://staging.crossmint.com";

    async function createOrder(walletAddress) {
        const res = await fetch(`${BASE_URL}/api/2022-06-09/orders`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "x-api-key": CROSSMINT_SERVER_API_KEY,
            },
            body: JSON.stringify({
                lineItems: {
                    // XMEME test token on Solana (staging only)
                    tokenLocator: "solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu",
                    executionParameters: {
                        mode: "exact-in",
                        amount: "5",
                        maxSlippageBps: "500",
                    },
                },
                payment: { method: "card" }, // wallet payment methods ride this rail at creation time
                recipient: { walletAddress },
            }),
        });

        const data = await res.json();
        if (!res.ok) {
            throw new Error(data.message ?? "Order creation failed");
        }
        return {
            orderId: data.order.orderId,
            clientSecret: data.clientSecret,
        };
    }
    ```

    The example uses a token line item; NFT collection line items work the same way. See the
    [create order API reference](/api-reference/headless/create-order) for all line item types and options.
  </Step>

  <Step title="Build the checkout URL">
    The checkout page accepts its configuration as URL query parameters:

    | Parameter      | Value                                                                          |
    | -------------- | ------------------------------------------------------------------------------ |
    | `orderId`      | From order creation                                                            |
    | `clientSecret` | From order creation                                                            |
    | `apiKey`       | Your **client** key (`ck_...`)                                                 |
    | `payment`      | URL-encoded JSON. Same options as the `payment` prop of the React component    |
    | `appearance`   | URL-encoded JSON. Same options as the `appearance` prop of the React component |

    ```javascript theme={null}
    const BASE_URL = "https://staging.crossmint.com"; // www.crossmint.com in production
    const CLIENT_API_KEY = "YOUR_CLIENT_API_KEY"; // ck_staging_...

    const payment = {
        crypto: { enabled: false },
        fiat: {
            enabled: true,
            allowedMethods: { applePay: true, card: false, googlePay: true },
        },
    };

    const appearance = {
        rules: {
            DestinationInput: { display: "hidden" },
            ReceiptEmailInput: { display: "hidden" },
            GlobalMessage: { display: "visible" },
        },
    };

    function buildCheckoutUrl(orderId, clientSecret) {
        const params = new URLSearchParams({
            orderId,
            clientSecret,
            payment: JSON.stringify(payment),
            appearance: JSON.stringify(appearance),
            apiKey: CLIENT_API_KEY,
        });
        return `${BASE_URL}/sdk/2024-03-05/embedded-checkout?${params.toString()}`;
    }
    ```

    Every configuration option from [payment methods](/payments/embedded/guides/payment-methods) and
    [UI customization](/payments/embedded/guides/ui-customization) works here. The example above renders a
    wallet-button-only checkout; see the
    [one-tap Apple Pay quickstart](/payments/embedded/quickstarts/apple-pay-only) for that experience end to end.
  </Step>

  <Step title="Render the URL in a WebView">
    A few WebView settings make the difference between a checkout that shows Apple Pay and Google Pay and one that silently does not. The tabs below contain the tested configuration per platform:

    <Tabs>
      <Tab title="React Native">
        Requires <a href="https://github.com/react-native-webview/react-native-webview" target="_blank" rel="noopener">react-native-webview</a> version 13.15.0 or higher.

        ```tsx theme={null}
        import { Platform } from "react-native";
        import { WebView } from "react-native-webview";

        // A standard mobile browser userAgent. The default WebView userAgent
        // identifies as an app, and the checkout hides wallet buttons for it.
        const userAgent = Platform.select({
            ios: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
            android:
                "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
        });

        export function CheckoutWebView({ checkoutUrl }: { checkoutUrl: string }) {
            return (
                <WebView
                    source={{ uri: checkoutUrl }}
                    userAgent={userAgent}
                    paymentRequestEnabled={true} // Google Pay; no effect on iOS
                    allowsInlineMediaPlayback={true}
                    mediaPlaybackRequiresUserAction={false}
                    domStorageEnabled={true}
                />
            );
        }
        ```

        For Google Pay, the app also needs the Android payment intent declaration from the
        [Google Pay guide](/payments/embedded/guides/google-pay).
      </Tab>

      <Tab title="Android (Kotlin)">
        Add the <a href="https://developer.android.com/jetpack/androidx/releases/webkit" target="_blank" rel="noopener">androidx.webkit</a> dependency to reach the Payment Request setting, which Google Pay requires and Android WebView ships disabled:

        ```kotlin theme={null}
        import android.webkit.WebChromeClient
        import android.webkit.WebView
        import android.webkit.WebViewClient
        import androidx.webkit.WebSettingsCompat
        import androidx.webkit.WebViewFeature

        fun configureCheckoutWebView(webView: WebView, checkoutUrl: String) {
            webView.settings.apply {
                javaScriptEnabled = true
                domStorageEnabled = true
                userAgentString =
                    "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
            }
            if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
                WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)
            }
            // Default clients keep every step of the checkout inside the WebView
            webView.webViewClient = WebViewClient()
            webView.webChromeClient = WebChromeClient()
            webView.loadUrl(checkoutUrl)
        }
        ```

        Declare the payment intent in `AndroidManifest.xml` so Google Pay can respond:

        ```xml theme={null}
        <manifest xmlns:android="http://schemas.android.com/apk/res/android">
            <queries>
                <intent>
                    <action android:name="org.chromium.intent.action.PAY" />
                </intent>
            </queries>
        </manifest>
        ```
      </Tab>

      <Tab title="iOS (Swift)">
        `WKWebView` needs inline media playback enabled; its other defaults work with the checkout as-is:

        ```swift theme={null}
        import WebKit

        func makeCheckoutWebView(checkoutUrl: URL) -> WKWebView {
            let configuration = WKWebViewConfiguration()
            configuration.allowsInlineMediaPlayback = true
            configuration.mediaTypesRequiringUserActionForPlayback = []

            let webView = WKWebView(frame: .zero, configuration: configuration)
            webView.load(URLRequest(url: checkoutUrl))
            return webView
        }
        ```

        <Note>
          If your app declares `WKAppBoundDomains` in its `Info.plist`, WebKit restricts every `WKWebView` to those
          domains. Add the domains listed under "Restricting navigation" below to that list, or the checkout cannot
          complete.
        </Note>
      </Tab>
    </Tabs>

    <Warning>
      Keep navigation inside the WebView unrestricted, and keep iframes enabled. To complete an order, the checkout
      navigates to pages from Crossmint's payment method and identity partners, and a WebView that blocks those
      pages interrupts the purchase with no visible error.
    </Warning>

    <Accordion title="Restricting navigation: the domains the checkout uses">
      If your security policy requires a navigation allowlist, include these domains, grouped by what they render
      during checkout:

      | Purpose                                  | Domains                                                                  |
      | ---------------------------------------- | ------------------------------------------------------------------------ |
      | Checkout and order pages                 | `crossmint.com`                                                          |
      | Card and wallet payment methods          | `stripe.com`, `checkout.com`, `pay.google.com`, `applepay.cdn-apple.com` |
      | Identity verification and risk screening | `withpersona.com`, `sardine.ai`                                          |

      Match subdomains as well (for example `*.crossmint.com`), and revisit the list when you update the SDK or
      change payment methods.
    </Accordion>
  </Step>

  <Step title="Track the order">
    The checkout page reports progress with messages. In React Native, listen with `onMessage`:

    ```tsx theme={null}
    <WebView
        // ...configuration from the previous step
        onMessage={(event) => {
            const message = JSON.parse(event.nativeEvent.data);
            // message.event is one of:
            //   order:updated              order phase changes; "delivery" means the purchase succeeded
            //   order:creation-failed     the order could not initialize
            //   ui:height.changed          the checkout has rendered content
            //   ui:express-checkout.ready  the wallet button is interactive
            if (message.event === "order:updated" && message.data?.order?.phase === "delivery") {
                // show your success state
            }
        }}
    />
    ```

    On iOS (Swift) and Android (Kotlin), track the order from your backend instead: subscribe to
    [webhooks](/payments/advanced/webhooks) or poll the
    [get order endpoint](/api-reference/headless/get-order) with the `orderId` you already hold.
  </Step>

  <Step title="Test on a physical device">
    Wallet payment methods render on real hardware: test Apple Pay on an iPhone running iOS 17 or higher with a card added to Apple Wallet, and Google Pay on an Android device with Google Play Services. In staging, wallet payments run against a test environment, so the card behind the wallet is never charged.

    To exercise the full purchase from a simulator or emulator during development, enable the card form
    (`allowedMethods: { card: true, ... }`) and pay with the staging test card `4242 4242 4242 4242`. More options are
    listed in [testing tips](/payments/advanced/testing-tips#test-credit-card-numbers).
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="The wallet button does not render">
    * Set a standard mobile browser `userAgent` on the WebView (step 3). The checkout serves wallet buttons to
      mobile browsers, and the default WebView userAgent identifies as an app.
    * For Google Pay, enable the Payment Request setting and declare the payment intent in the manifest (step 3),
      and test on a device with Google Play Services.
    * For Apple Pay, use a physical iPhone on iOS 17 or higher: the iOS Simulator does not render Apple Pay.
    * Confirm the order was created successfully and `payment` in the URL enables the wallet you expect.
  </Accordion>

  <Accordion title="The payment sheet opens but the purchase never completes">
    A navigation restriction is usually interrupting the checkout mid-purchase. Remove custom
    `WebViewClient`/`WKNavigationDelegate` rules that cancel navigations, or extend your allowlist with the
    domains listed under "Restricting navigation" above.
  </Accordion>

  <Accordion title="The checkout page does not load">
    * Enable JavaScript and DOM storage on the WebView (step 3).
    * Match the base URL to the key environment: `ck_staging_`/`sk_staging_` keys pair with
      `staging.crossmint.com`, production keys with `www.crossmint.com`.
    * Confirm the `payment` and `appearance` parameters contain valid JSON after URL encoding.
  </Accordion>

  <Accordion title="A payment fails and nothing appears on screen">
    Pass `GlobalMessage: { display: "visible" }` inside `appearance.rules` (step 2). In layouts that hide the
    checkout inputs, this rule is the surface where payment errors render.
  </Accordion>
</AccordionGroup>

## FAQ

<AccordionGroup>
  <Accordion title="Can a Crossmint SDK handle this for me?">
    Yes. The [React Native](/payments/embedded/quickstarts/credit-card-memecoin-react-native),
    [Flutter](/payments/embedded/quickstarts/credit-card-memecoin-flutter), and
    [Kotlin](/sdk-reference/checkout/kotlin/index) SDKs apply this WebView configuration automatically and expose
    the checkout as a component. Use this guide when you need your own WebView, or on platforms without an SDK,
    such as native iOS (Swift).
  </Accordion>

  <Accordion title="Do I need to register a domain for Apple Pay?">
    No. [Apple Pay domain registration](/payments/embedded/guides/apple-pay) applies to websites that embed the
    checkout. In this integration the checkout page is served from `crossmint.com`, which is already enabled for
    Apple Pay.
  </Accordion>

  <Accordion title="How do I render only the Apple Pay or Google Pay button?">
    Restrict the payment methods and hide the checkout inputs through the `payment` and `appearance` URL
    parameters. The [one-tap Apple Pay quickstart](/payments/embedded/quickstarts/apple-pay-only) covers the
    complete configuration, and every setting in it maps directly onto the URL parameters from step 2.
  </Accordion>

  <Accordion title="Can I open the checkout in a browser instead of a WebView?">
    Yes. The checkout URL also renders in `SFSafariViewController`, Chrome Custom Tabs, or a regular browser tab.
    A WebView keeps the checkout inside your own screens; a browser surface takes less configuration and suits
    flows where opening a sheet over the app is acceptable.
  </Accordion>

  <Accordion title="Which payment methods can users see?">
    The same ones as the embedded checkout on web: cards, Apple Pay, Google Pay, and crypto, controlled by the
    `payment` URL parameter. See [payment methods](/payments/embedded/guides/payment-methods).
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="One-Tap Apple Pay" icon="apple" href="/payments/embedded/quickstarts/apple-pay-only">
    Render only the wallet button and build the rest of the experience in your own UI
  </Card>

  <Card title="Google Pay Mobile" icon="google" href="/payments/embedded/guides/google-pay">
    Production approval and native Android configuration for Google Pay
  </Card>

  <Card title="UI Customization" icon="paintbrush" href="/payments/embedded/guides/ui-customization">
    Theme the checkout through the appearance parameter
  </Card>
</CardGroup>
