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

# UPI Intent in iOS

> Embed a Cashfree WKWebView-based custom checkout in your iOS app and enable UPI Intent so customers can pay using GPay, PhonePe, Paytm, and other UPI apps.

Cashfree's iOS custom checkout integration enables merchants to embed a secure payment experience directly within their iOS applications using WKWebView, supporting a wide variety of payment methods.

### Key benefits

* **Secure and PCI compliant**: Securely collects payment details and submits them directly to Cashfree servers, removing the need for Payment Card Industry Data Security Standard (PCI DSS) compliance requirements at the merchant's end.

* **Native iOS integration**: Seamlessly integrate payments into your iOS app using WKWebView without requiring third-party SDKs.

* **Support for multiple payment methods**: Accepts payments from over 120 payment methods including UPI Intent for direct app-to-app transfers.

### Prerequisites

1. Create a [Cashfree Merchant Account](https://merchant.cashfree.com/merchants/signup) and log in.
2. Navigate to **Developers > API Keys** to generate your **App ID** and **Secret Key**. Learn how to [generate API keys](/api-reference/authentication#generate-api-keys).
3. Minimum iOS version is iOS 11.0 with WKWebView support.

The iOS checkout integration consists of three essential steps:

<CardGroup cols={3}>
  <Card title="Step 1" icon="cube" href="/payments/online/web/custom-checkout-ios#step-1:-create-order-server-side">
    Create Order
  </Card>

  <Card title="Step 2" icon="code" href="/payments/online/web/custom-checkout-ios#step-2:-submit-form-client-side">
    Submit Form
  </Card>

  <Card title="Step 3" icon="check" href="/payments/online/web/custom-checkout-ios#step-3:-handle-redirection-client-side">
    Handle Redirection
  </Card>
</CardGroup>

## Step 1: Create order <Badge color="green">Server-side</Badge>

Create an order from your backend before processing the payment. This is a server-side operation and must not be called from the client side.

<Note>
  * Create orders through your backend, as this API requires your secret key.
  * Always provide a return URL when creating the order—it will be used to detect when the payment flow is complete.
</Note>

You can follow the order creation steps in the [Cashfree Hosted Checkout](/payments/online/web/redirect#step-1-creating-an-order) documentation. This will return a "payment\_session\_id" which will be used in the subsequent steps.

## Step 2: Submit form <Badge color="orange">Client-side</Badge>

Submit the checkout form from your app's web view using the payment session ID obtained from Step 1.

<Note>
  This step requires your application's bundle ID to be whitelisted by Cashfree Payments. Please check [Bundle ID Whitelisting](/payments/online/go-live/whitelist).
</Note>

```swift Swift theme={"dark"}
String platform = "iosx-c-x-x-x-w-x-a-" + <BuildVersion>;
String session = "PAYMENT_SESSION_ID";

func createForm(session: String, platform: String) -> String {
        var secureURL = ""
        if CFConstants.environment == .SANDBOX {
            secureURL = "https://sandbox.cashfree.com/pg/view/sessions/checkout"
        } else {
            secureURL = "https://api.cashfree.com/pg/view/sessions/checkout"
        }
        var paymentInputFormHtml =
            "<html>"
                + "<body>"
                + "<form id=\'redirectForm\' name=\'order\' action=\'\(secureURL)\' method=\'post\'>"
        paymentInputFormHtml += "<input type=\'hidden\' name=\'payment_session_id\' value=\'\(session)\'/>" +
            "<input type=\'hidden\' name=\'platform\' value=\'\(platform)\'/>" +
            "</form>" +
        "<script\n" +
                        " type=\"text/javascript\">\t window.onload = function () { const form = document.getElementById(\"redirectForm\"); const meta = { userAgent: window.navigator.userAgent, }; const sortedMeta = Object.entries(meta).sort().reduce((o, [k, v]) => { o[k] = v; return o; }, {}); const base64Meta = btoa(JSON.stringify(sortedMeta)); FN = document.createElement('input'); FN.setAttribute('type', 'hidden'); FN.setAttribute('name', 'browser_meta'); FN.setAttribute('value', base64Meta); form.appendChild(FN); form.submit(); }  </script>\n"
            + "</body>"
            + "</html>";
        return paymentInputFormHtml
    }
override func viewDidLoad(){
self.cfWebView.loadHTMLString(createForm(session: session_id, platform: "") , baseURL: nil)
}
```

## Step 3: Handle redirection <Badge color="orange">Client-side</Badge>

Once the payment flow has ended, Cashfree Payments will redirect you to the URL specified while creating the order. Detect the URL redirection and close the web view appropriately.

```swift Swift theme={"dark"}
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    
    // Allowing all navigations
    decisionHandler(.allow)
    
    if navigationAction.request.url?.absoluteString ?? "" == "your return_url" {
        // close the web view
    }
}
```

## UPI intent from checkout

UPI Intent allows users to make direct payments through installed UPI applications without entering payment details manually. If you want to add UPI intent functionality to your custom iOS checkout integration, follow the steps below.

1. Add the required UPI app schemes to your **info.plist** file.\
   You need to add the following to your **info.plist** to enable your app to detect installed UPI applications:
   ```xml swift theme={"dark"}
   <key>LSApplicationCategoryType</key>
   <string></string>
   <key>LSApplicationQueriesSchemes</key>
   <array>
     <string>bhim</string>
     <string>paytmmp</string>
     <string>phonepe</string>
     <string>tez</string>
     <string>credpay</string>
   </array>
   ```

2. Add the JSBridge functions for the checkout page to get the list of installed UPI apps and handle opening the selected UPI app.

   ```swift Swift theme={"dark"}
   override func viewDidLoad() {

           super.viewDidLoad()
           self.cfWebView.configuration.userContentController.add(self, name: "nativeProcess")
           self.cfWebView.uiDelegate = self
           self.cfWebView.navigationDelegate = self
   }

   func getInstalledUPIApplications() -> [[String: String]] {
           var installedApps = [[String: String]]()
           let appName = [
               [
                   "displayName": CFConstants.googlePayName,
                   "id": "tez://",
                   "icon": (UIImage(named: "gpay", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
               ],
               [
                   "displayName": CFConstants.paytmName,
                   "id": "paytmmp://",
                   "icon": (UIImage(named: "paytm", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
               ],
               [
                   "displayName": CFConstants.phonePeName,
                   "id": "phonepe://",
                   "icon": (UIImage(named: "phonepe", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
               ],
               [
                   "displayName": CFConstants.bhimName,
                   "id": "bhim://",
                   "icon": (UIImage(named: "bhim", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
               ],
               [
                   "displayName": CFConstants.credName,
                   "id": "credpay://",
                   "icon": (UIImage(named: "credpay", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
               ]
           ]
           for app in appName {
               let url = app["id"] ?? ""
               if UIApplication.shared.canOpenURL(URL(string: url)!) {
                   installedApps.append(app)
               }
           }
           return installedApps
       }

   public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
           var msg = message.body as? String ?? ""
           if msg == "getAppList" {
               let installedApps = getInstalledUPIApplications()
               do {
                   let jsonData = try JSONSerialization.data(withJSONObject: installedApps, options: [])
                   let jsonString = String(data: jsonData, encoding: String.Encoding.utf8)!
                   self.cfWebView.evaluateJavaScript("window.receiveAppList(\(jsonString))") { (_, _) in
                   }
               } catch {
                   print("Serialisation Failed")
               }
           } else if msg.contains("paytm") || msg.contains("phonepe") || msg.contains("tez") || msg.contains("bhim") || msg.contains("credpay") {
               if msg.contains("paytm") {
                   msg = msg.replacingOccurrences(of: "paytm:", with: "paytmmp:")
               }
               self.cfWebView.evaluateJavaScript("verifyPaymentForiOS()") { (val, error) in
                   let url = URL(string: msg)!
                   if UIApplication.shared.canOpenURL(url) {
                       UIApplication.shared.open(url, options: [:]) { (response) in
                       }
                   }
               }
           }
       }
   }
   ```

## Testing

After completing the iOS integration, verify that the payment flow works correctly:

1. Create a test order from your backend with a valid return URL.
2. Load the checkout form in your WKWebView with the payment session ID.
3. Verify that the payment page loads in the web view.
4. Test the payment flow end-to-end using test credentials.
5. Verify that the return URL is triggered correctly after payment completion.
6. If using UPI Intent, test with actual UPI applications installed on your test device.

## Sample code

```swift Swift theme={"dark"}
class CF2FAWebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {

@IBOutlet weak var cfWebView: WKWebView!

override func viewDidLoad() {

        super.viewDidLoad()
        self.cfWebView.configuration.userContentController.add(self, name: "nativeProcess")
        self.cfWebView.uiDelegate = self
        self.cfWebView.navigationDelegate = self
        self.cfWebView.loadHTMLString(createForm(session: session_id, platform: "") , baseURL: nil)
}


func getInstalledUPIApplications() -> [[String: String]] {
        var installedApps = [[String: String]]()
        let appName = [
            [
                "displayName": CFConstants.googlePayName,
                "id": CFConstants.googlePayPackage,
                "icon": (UIImage(named: "gpay", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
            ],
            [
                "displayName": CFConstants.paytmName,
                "id": CFConstants.paytmPackage,
                "icon": (UIImage(named: "paytm", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
            ],
            [
                "displayName": CFConstants.phonePeName,
                "id": CFConstants.phonePePackage,
                "icon": (UIImage(named: "phonepe", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
            ],
            [
                "displayName": CFConstants.bhimName,
                "id": CFConstants.bhimPackage,
                "icon": (UIImage(named: "bhim", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
            ],
            [
                "displayName": CFConstants.credName,
                "id": CFConstants.credPackage,
                "icon": (UIImage(named: "credpay", in: Bundle(for: CFUPIUtils.self), compatibleWith: nil)!).toBase64() ?? ""
            ]
        ]
        for app in appName {
            let url = app["id"] ?? ""
            if UIApplication.shared.canOpenURL(URL(string: url)!) {
                installedApps.append(app)
            }
        }
        return installedApps
    }

public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    
    // Allowing all navigations
    decisionHandler(.allow)
    
    if navigationAction.request.url?.absoluteString ?? "" == "your return_url" {
        // close the web view
    }
}

func createForm(session: String, platform: String) -> String {
        var secureURL = ""
        if CFConstants.environment == .SANDBOX {
            secureURL = "https://sandbox.cashfree.com/pg/view/sessions/checkout"
//            secureURL = "https://test-k8s.cashfree.com/pgnextgenapi/api/v1/view/sessions/checkout"
        } else {
            secureURL = "https://api.cashfree.com/pg/view/sessions/checkout"
//            secureURL = "https://prod.cashfree.com/pgnextgenapi-test/api/v1/view/sessions/checkout"
        }
        var paymentInputFormHtml =
            "<html>"
                + "<body>"
                + "<form id=\'redirectForm\' name=\'order\' action=\'\(secureURL)\' method=\'post\'>"
        paymentInputFormHtml += "<input type=\'hidden\' name=\'payment_session_id\' value=\'\(session)\'/>" +
            "<input type=\'hidden\' name=\'platform\' value=\'\(platform)\'/>" +
            "</form>" +
        "<script\n" +
                        " type=\"text/javascript\">\t window.onload = function () { const form = document.getElementById(\"redirectForm\"); const meta = { userAgent: window.navigator.userAgent, }; const sortedMeta = Object.entries(meta).sort().reduce((o, [k, v]) => { o[k] = v; return o; }, {}); const base64Meta = btoa(JSON.stringify(sortedMeta)); FN = document.createElement('input'); FN.setAttribute('type', 'hidden'); FN.setAttribute('name', 'browser_meta'); FN.setAttribute('value', base64Meta); form.appendChild(FN); form.submit(); }  </script>\n"
            + "</body>"
            + "</html>";
        return paymentInputFormHtml
    }


public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        var msg = message.body as? String ?? ""
        if msg == "getAppList" {
            let installedApps = getInstalledUPIApplications()
            do {
                let jsonData = try JSONSerialization.data(withJSONObject: installedApps, options: [])
                let jsonString = String(data: jsonData, encoding: String.Encoding.utf8)!
                self.cfWebView.evaluateJavaScript("window.receiveAppList(\(jsonString))") { (_, _) in
                }
            } catch {
                print("Serialisation Failed")
            }
        } else if msg.contains("paytm") || msg.contains("phonepe") || msg.contains("tez") || msg.contains("bhim") || msg.contains("credpay") {
            if msg.contains("paytm") {
                msg = msg.replacingOccurrences(of: "paytm:", with: "paytmmp:")
            }
            self.cfWebView.evaluateJavaScript("verifyPaymentForiOS()") { (val, error) in
                let url = URL(string: msg)!
                if UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url, options: [:]) { (response) in
                    }
                }
            }
        }
    }
}
```

<snippet>snippets/related-topics-loader.mdx</snippet>

<div class="hidden" data-table-of-contents="bottom">
  <p class="mt-4 font-medium flex items-center gap-2 related-docs-heading">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="w-4 h-4">
      <path d="M3 4h7a2 2 0 0 1 2 2v13a2 2 0 0 0-2-2H3z" />

      <path d="M21 4h-7a2 2 0 0 0-2 2v13a2 2 0 0 1 2-2h7z" />
    </svg>

    <span>Related topics</span>
  </p>

  <ul>
    <li><a href="/docs/api-reference/payments/latest/orders/create">Create Order API</a></li>
    <li><a href="/docs/payments/online/go-live/whitelist">Domain Whitelisting</a></li>
    <li><a href="/docs/payments/manage/payment-methods#upi">UPI Intent Methods</a></li>
    <li><a href="/docs/payments/manage/payment-methods">Payment Methods</a></li>
    <li><a href="/docs/payments/online/web/redirect">Hosted Checkout Integration</a></li>
  </ul>
</div>
