{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-products/hosted-checkout/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Embedded Checkout (iFrame) — Mobile Implementation","description":"Official Banxa API documentation – on-ramp and off-ramp transfers with identity verification and compliance.","llmstxt":{"title":"Banxa Developer Documentation","description":"Integrate crypto-fiat exchange with Banxa's licensed infrastructure: payments, KYC, compliance, and settlement handled. 150+ countries, 45 global licences.","details":{"content":"Two integration products: **Banxa Native API** for partners who manage their own KYC and want full UX control (headless, HMAC server-to-server, no Banxa-hosted screens); **Banxa Hosted Checkout** for partners who want Banxa to handle KYC and payments (three paths: Referral URL, API, or React Native SDK). Both use the same sandbox and production environments at `https://api.banxa-sandbox.com` and `https://api.banxa.com`.\n\n## Constraints\n\n- **Authentication**: HMAC credentials must be stored server-side only, never expose in frontend, mobile, or client-side code. HMAC is required for all Native API calls and for KYC sharing in Hosted Checkout; `x-api-key` is used for all other Hosted Checkout endpoints.\n- **`externalCustomerId`**: Required on every buy and sell order. Use a stable opaque identifier, never PII.\n- **`identityReference`**: Must remain constant for the same user across all requests. Must not contain PII. If you attempt to create an identity for an email that already exists, you will receive a 422 / code 81, so retrieve the existing record rather than retrying creation. Use `GET /eapi/v0/identities/{identityReference}?email=user@example.com` to look up the real `identityReference` linked to an email.\n- **`quoteId`**: Only supported by `POST /eapi/v0/ramps` (bank transfer). The React Native SDK and Embedded Payment Button do not accept a `quoteId`, so use indicative pricing (`GET /eapi/v0/price`) for SDK and Embedded Payment Button flows.\n- **Quotes**: Indicative prices are not rate-locked, so refresh close to order creation to minimise rate drift. Locked quotes (`GET /eapi/v0/quote`) expire after approximately 3 minutes and are only valid for bank transfer ramp creation.\n- **Eligibility gate**: Never create a ramp or invoke the SDK when `paymentReady` is `false`. Always check eligibility and satisfy all requirements before payment execution.\n- **Webhooks**: Verify all inbound webhook signatures with HMAC-SHA256 before processing. Return HTTP 200 immediately and process asynchronously.\n- **Product selection**: Banxa Native is for partners who manage their own KYC. Banxa Hosted Checkout is for partners who do not do KYC. These are separate products with separate flows: do not mix endpoints across products.\n- **SDK scope**: The React Native SDK has no `banxa.customerIdentity` module. Identity and KYC are handled through the Native API only. `primerCallbacks` and `primerSettings` are Native context only and must not be referenced in Hosted Checkout integrations.\n- **Payment method naming**: Never use \"eAPI\" or \"EAPI\" as a product name. The correct name is \"Banxa Native API\". The URL path prefix `/eapi/` is correct and should not be changed.\n"},"hide":false,"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"embedded-checkout-iframe--mobile-implementation","__idx":0},"children":["Embedded Checkout (iFrame) — Mobile Implementation"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Loading the Banxa checkout in a mobile WebView requires specific configuration to support camera access, payment methods, and KYC flows."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"android","__idx":1},"children":["Android"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"use-custom-chrome-tabs-recommended","__idx":2},"children":["Use Custom Chrome Tabs (recommended)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If your integration includes Google Pay, use ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Custom Chrome Tabs"]}," instead of a standard WebView. Standard WebView does not support GPAY or ACH."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"kotlin","header":{"controls":{"copy":{}}},"source":"import androidx.browser.customtabs.CustomTabsIntent\n\nval customTabsIntent = CustomTabsIntent.Builder().build()\ncustomTabsIntent.launchUrl(context, Uri.parse(checkoutUrl))\n","lang":"kotlin"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"webview-for-integrations-without-google-pay","__idx":3},"children":["WebView (for integrations without Google Pay)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you are not using Google Pay, you can use a WebView with the following configuration:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"kotlin","header":{"controls":{"copy":{}}},"source":"webView.settings.apply {\n    javaScriptEnabled = true\n    domStorageEnabled = true          // Required: enables local storage\n    mediaPlaybackRequiresUserGesture = false  // Required: enables video instructions\n    allowFileAccess = true\n    setSupportZoom(false)\n}\n\n// Enable camera access\nwebView.webChromeClient = object : WebChromeClient() {\n    override fun onPermissionRequest(request: PermissionRequest) {\n        request.grant(request.resources)  // Grant camera/microphone\n    }\n\n    // Required for HTML5 video playback\n    override fun onShowCustomView(view: View, callback: CustomViewCallback) {\n        // Handle fullscreen video\n    }\n}\n","lang":"kotlin"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Declare permissions in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AndroidManifest.xml"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"xml","header":{"controls":{"copy":{}}},"source":"<uses-permission android:name=\"android.permission.CAMERA\" />\n<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n<uses-permission android:name=\"android.permission.INTERNET\" />\n","lang":"xml"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"ios","__idx":4},"children":["iOS"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"sfsafariviewcontroller-recommended","__idx":5},"children":["SFSafariViewController (recommended)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SFSafariViewController"]}," when your integration includes KYC liveness checks (Sumsub). Liveness checks cannot complete in a standard ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WKWebView"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"swift","header":{"controls":{"copy":{}}},"source":"import SafariServices\n\nlet safariVC = SFSafariViewController(url: URL(string: checkoutUrl)!)\npresent(safariVC, animated: true)\n","lang":"swift"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"wkwebview","__idx":6},"children":["WKWebView"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If liveness checks are not required, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WKWebView"]}," can be used with the following configuration:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"swift","header":{"controls":{"copy":{}}},"source":"import WebKit\n\nlet config = WKWebViewConfiguration()\nconfig.allowsInlineMediaPlayback = true          // Required\nconfig.mediaTypesRequiringUserActionForPlayback = []  // Required: allow autoplay for video instructions\n\nlet webView = WKWebView(frame: .zero, configuration: config)\n\n// Request camera permission\nwebView.uiDelegate = self  // Implement WKUIDelegate\n","lang":"swift"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Implement ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WKUIDelegate"]}," to handle permission requests:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"swift","header":{"controls":{"copy":{}}},"source":"func webView(_ webView: WKWebView,\n             requestMediaCapturePermissionFor origin: WKSecurityOrigin,\n             initiatedByFrame frame: WKFrameInfo,\n             type: WKMediaCaptureType,\n             decisionHandler: @escaping (WKPermissionDecision) -> Void) {\n    decisionHandler(.grant)\n}\n","lang":"swift"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Add to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Info.plist"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"xml","header":{"controls":{"copy":{}}},"source":"<key>NSCameraUsageDescription</key>\n<string>Required for identity verification</string>\n<key>NSMicrophoneUsageDescription</key>\n<string>Required for identity verification</string>\n","lang":"xml"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"react-native","__idx":7},"children":["React Native"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["react-native-webview"]}," with the following props:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"jsx","header":{"controls":{"copy":{}}},"source":"import { WebView } from 'react-native-webview';\n\n<WebView\n  source={{ uri: checkoutUrl }}\n  allowsInlineMediaPlayback={true}        // Required: inline video for instructions\n  mediaPlaybackRequiresUserAction={false} // Required: allow autoplay\n  domStorageEnabled={true}               // Required: local storage (Android)\n  enableApplePay={true}                  // Required: Apple Pay on iOS\n  onNavigationStateChange={(navState) => {\n    // Detect redirect to your redirectUrl\n    if (navState.url.startsWith('https://yourapp.com/order-complete')) {\n      // Dismiss WebView and check order status\n    }\n  }}\n/>\n","lang":"jsx"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Camera and microphone access is controlled by OS-level permissions — set in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AndroidManifest.xml"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Info.plist"]}," as shown below, not via WebView props."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"flutter","__idx":8},"children":["Flutter"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webview_flutter"]}," with the following configuration:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"dart","header":{"controls":{"copy":{}}},"source":"import 'package:webview_flutter/webview_flutter.dart';\n\nlate final WebViewController controller;\n\ncontroller = WebViewController()\n  ..setJavaScriptMode(JavaScriptMode.unrestricted)\n  ..setNavigationDelegate(NavigationDelegate(\n    onNavigationRequest: (NavigationRequest request) {\n      if (request.url.startsWith('https://yourapp.com/order-complete')) {\n        // Dismiss WebView\n        return NavigationDecision.prevent;\n      }\n      return NavigationDecision.navigate;\n    },\n  ))\n  ..loadRequest(Uri.parse(checkoutUrl));\n","lang":"dart"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For camera access on Android, add to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AndroidManifest.xml"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"xml","header":{"controls":{"copy":{}}},"source":"<uses-permission android:name=\"android.permission.CAMERA\" />\n","lang":"xml"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"payment-methods-that-redirect-out-of-the-webview","__idx":9},"children":["Payment methods that redirect out of the WebView"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["iDEAL, Klarna, and PayPal cannot complete inside a WebView. When a customer selects one of these methods, they will be redirected to an external browser to complete their payment. After payment, the customer is returned to the Banxa order status page — they will not be returned automatically to your WebView."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Design your post-payment UX accordingly: a confirmation screen with a link back to your app is the recommended pattern."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"configuration-checklist","__idx":10},"children":["Configuration checklist"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Requirement"},"children":["Requirement"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Android"},"children":["Android"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"iOS"},"children":["iOS"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Local storage"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["domStorageEnabled = true"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Enabled by default in WKWebView"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Camera access"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onPermissionRequest"]}," grant"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WKUIDelegate"]}," + Info.plist"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Video playback"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WebChromeClient"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["mediaPlaybackRequiresUserGesture = false"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["allowsInlineMediaPlayback = true"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["KYC liveness"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Custom Chrome Tabs or WebView"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["SFSafariViewController required"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Google Pay"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Custom Chrome Tabs required"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Supported in WKWebView"]}]}]}]}]}]},"headings":[{"value":"Embedded Checkout (iFrame) — Mobile Implementation","id":"embedded-checkout-iframe--mobile-implementation","depth":1},{"value":"Android","id":"android","depth":2},{"value":"Use Custom Chrome Tabs (recommended)","id":"use-custom-chrome-tabs-recommended","depth":3},{"value":"WebView (for integrations without Google Pay)","id":"webview-for-integrations-without-google-pay","depth":3},{"value":"iOS","id":"ios","depth":2},{"value":"SFSafariViewController (recommended)","id":"sfsafariviewcontroller-recommended","depth":3},{"value":"WKWebView","id":"wkwebview","depth":3},{"value":"React Native","id":"react-native","depth":2},{"value":"Flutter","id":"flutter","depth":2},{"value":"Payment methods that redirect out of the WebView","id":"payment-methods-that-redirect-out-of-the-webview","depth":2},{"value":"Configuration checklist","id":"configuration-checklist","depth":2}],"frontmatter":{"title":"Embedded Checkout: Mobile WebView Setup | Banxa Docs","description":"Configure Android WebView and iOS WKWebView for Banxa embedded checkout. Camera access, video playback, KYC liveness, and React Native/Flutter WebView examples.","seo":{"title":"Embedded Checkout (iFrame) — Mobile Implementation"}},"lastModified":"2026-05-19T23:30:38.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/products/hosted-checkout/docs/checkout-experience/iframe/webview-mobile","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}