Skip to main content
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.

Why choose Cashfree?

  • 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 and log in.
  2. Navigate to Developers > API Keys to generate your App ID and Secret Key. Learn how to generate API keys.
  3. Minimum iOS version is iOS 11.0 with WKWebView support.
The iOS checkout integration consists of three essential steps:

Step 1: Create order Server-side

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.
  • Order creation must be done 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.
You can follow the order creation steps in the Cashfree Hosted Checkout documentation. This will return a “payment_session_id” which will be used in the subsequent steps.

Step 2: Submit form Client-side

Submit the checkout form from your app’s web view using the payment session ID obtained from Step 1.
This step requires your application’s bundle ID to be whitelisted by Cashfree Payments. Please check Bundle ID Whitelisting.
Swift
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 Client-side

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
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:
    swift
    <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
    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
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
                    }
                }
            }
        }
    }
}