Apple Pay WKWebView Integration
Integrate Apple Pay in iOS WKWebView using the Razorpay Web Component.
Apple Pay is supported inside WKWebView since iOS 11.3, but availability depends on the context in which the WebView is running.
For Apple Pay to work in your app's WKWebView, ensure the following:
- Apple Pay entitlement: The host app must have
com.apple.developer.apple-paycapability enabled in Xcode. Without this,ApplePaySessionwill be undefined in the WebView's JavaScript context. - iOS 11.3+: Minimum OS version for Apple Pay JS API version 3 in WKWebView.
- HTTPS: The page loaded in the WebView must be served over HTTPS.
- Domain verification: The domain served in the WebView must be registered and verified. Refer to the guide.
- Card in Apple Wallet: The user must have at least one card added to Apple Wallet with face id, touch id or passcode enabled.
Handy Tip
There are no WebView-specific restrictions in this SDK. The same eligibility checks the device support, business configuration and card network availability, apply in both Safari and WKWebView. If all prerequisites above are met, Apple Pay will work in a WKWebView exactly as it does in Safari.
If your app has a native checkout screen and you want to embed the Apple Pay button at a specific position and size, follow the steps given below.
Create a lightweight HTML page on the same domain you registered for Apple Pay verification. The app's WKWebView will load this URL directly, this ensures window.location.hostname matches your verified domain, which is required for business validation.
https://your-domain.com/apple-pay.html
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"><script src="https://checkout.razorpay.com/checkout-blocks/checkout-blocks.js"></script><style>* { margin: 0; padding: 0; }body { background: transparent; overflow: hidden; }</style></head><body><rzp-digital-walletid="wallet"method="apple_pay"button-color-scheme="dark"button-type="pay"></rzp-digital-wallet></body></html>
// Read config from URL query params passed by the native appconst params = new URLSearchParams(window.location.search);const wallet = document.getElementById('wallet');wallet.setAttribute('rzp-key', params.get('key'));wallet.setAttribute('order-id', params.get('order_id'));wallet.setAttribute('customer-contact-number', params.get('contact'));wallet.setAttribute('button-width', params.get('width') || '100%');wallet.setAttribute('button-height', params.get('height') || '44px');// Forward events to the native app via webkit message handlerwallet.addEventListener('paymentsuccess', (e) => {webkit.messageHandlers.paymentEvent.postMessage({type: 'success',data: e.detail.paymentData});});wallet.addEventListener('paymentfailure', (e) => {webkit.messageHandlers.paymentEvent.postMessage({type: 'failure',data: e.detail.error});});wallet.addEventListener('error', (e) => {webkit.messageHandlers.paymentEvent.postMessage({type: 'error',data: { code: e.detail.errorCode, message: e.detail.message }});});
The page reads key, order_id, contact, width and height from query parameters, so the native app controls all configuration without changing the hosted HTML.
import UIKitimport WebKitclass CheckoutViewController: UIViewController, WKScriptMessageHandler {private var applePayWebView: WKWebView!// Button size in points — the WebView frame and the component stay in syncprivate let buttonWidth: CGFloat = 300private let buttonHeight: CGFloat = 44override func viewDidLoad() {super.viewDidLoad()let config = WKWebViewConfiguration()config.preferences.javaScriptEnabled = trueconfig.userContentController.add(self, name: "paymentEvent")applePayWebView = WKWebView(frame: CGRect(x: 0, y: 0, width: buttonWidth, height: buttonHeight),configuration: config)applePayWebView.scrollView.isScrollEnabled = falseapplePayWebView.isOpaque = falseapplePayWebView.backgroundColor = .clearapplePayWebView.scrollView.backgroundColor = .clear// Position it within your native checkout layoutview.addSubview(applePayWebView)loadApplePayButton(rzpKey: "rzp_test_XXXXXXXXXX",orderId: "order_XXXXXXXXXX", // created via your servercontact: "+919876543210")}private func loadApplePayButton(rzpKey: String, orderId: String, contact: String) {var components = URLComponents(string: "https://your-domain.com/apple-pay.html")!components.queryItems = [URLQueryItem(name: "key", value: rzpKey),URLQueryItem(name: "order_id", value: orderId),URLQueryItem(name: "contact", value: contact),URLQueryItem(name: "width", value: "\(Int(buttonWidth))px"),URLQueryItem(name: "height", value: "\(Int(buttonHeight))px"),]applePayWebView.load(URLRequest(url: components.url!))}func userContentController(_ userContentController: WKUserContentController,didReceive message: WKScriptMessage) {guard let body = message.body as? [String: Any],let type = body["type"] as? String,let data = body["data"] as? [String: Any] else { return }switch type {case "success":let paymentId = data["razorpay_payment_id"] as? String ?? ""let orderId = data["razorpay_order_id"] as? String ?? ""let signature = data["razorpay_signature"] as? String ?? ""// Always verify signature on your server before fulfilling the orderhandlePaymentSuccess(paymentId: paymentId, orderId: orderId, signature: signature)case "failure":let code = data["code"] as? String ?? ""let message = data["message"] as? String ?? ""if code == "PAYMENT_CANCELLED" {handlePaymentCancelled()} else {handlePaymentFailure(code: code, message: message)}case "error":let message = data["message"] as? String ?? ""handleComponentError(message: message)default:break}}}
The SDK sends window.location.hostname as the initiativeContextUrl during Apple Pay business validation. When HTML is loaded via loadHTMLString, the hostname may not match your verified domain, causing business validation to fail. Loading a real URL on your verified domain avoids this entirely.
Is this integration guide useful?