Mobile app integration

How to open Connect from an Android or iOS app so bank apps open and the payer returns to your order.

Connect is a web page. In a mobile app, the way you open it decides whether the payer can reach their bank and come back to your order.

On mobile, most banks authenticate the payer in their own app: the bank page hands off to it through an app link or a custom URL scheme (for example bnpp:// or itsme://). An embedded webview cannot perform that handoff by itself, so the payment stops at the bank step.

Recommended: open Connect in a browser surface

Use the system browser drawn inside your app. It keeps the operating system's link rules, so bank apps open and return normally.

PlatformComponent
AndroidChrome Custom Tabs
iOSASWebAuthenticationSession (preferred) or SFSafariViewController
React Native, Flutter, CapacitorThe InAppBrowser or Browser plugin (a system tab), not the framework's webview
val tab = CustomTabsIntent.Builder().build()
tab.launchUrl(context, Uri.parse(connectUrl))
let session = ASWebAuthenticationSession(
    url: connectUrl,
    callback: .https(host: "checkout.merchant.com", path: "/pay/return")
) { url, error in
    // Read session_id and state from url, then confirm the payment server-side
}
session.presentationContextProvider = self
session.start()

Register your redirect_uri as an Android App Link and an iOS Universal Link so the return from the bank reopens your app.

💡 Note

The https callback of ASWebAuthenticationSession requires iOS 17.4. Below that version, use SFSafariViewController and handle the return through your Universal Link.

If you must use a webview

🚧 Important

A stock WebView or WKWebView does not open bank apps: custom schemes fail with ERR_UNKNOWN_URL_SCHEME and app links load as web pages. Banks whose mobile flow requires their app cannot be paid from it. Your app must implement the four rules below.

  1. Hand every URL that is not http or https to the operating system (bank app schemes, intent://).
  2. Open any host that is not *.fintecture.com outside the webview, so app links can fire.
  3. Intercept navigations to your redirect_uri and origin_uri: close the webview and show your order.
  4. Confirm on the server and re-read the payment status when your app returns to the foreground.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView, req: WebResourceRequest): Boolean {
        val url = req.url
        // 3. Your return URL
        if (url.host == "checkout.merchant.com" && url.path?.startsWith("/pay/return") == true) {
            onPaymentReturn(url); return true
        }
        // 1. Non-http(s) schemes belong to the OS
        if (url.scheme != "http" && url.scheme != "https") { openExternally(url); return true }
        // 2. The bank's domain leaves the webview
        if (!url.host.orEmpty().endsWith("fintecture.com")) { openExternally(url); return true }
        return false
    }
}

fun openExternally(uri: Uri) {
    val intent = if (uri.scheme == "intent") Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME)
                 else Intent(Intent.ACTION_VIEW, uri)
    intent.addCategory(Intent.CATEGORY_BROWSABLE)
    try { startActivity(intent) } catch (e: ActivityNotFoundException) { openInCustomTab(uri) }
}
func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    guard let url = action.request.url else { return decisionHandler(.allow) }
    // 3. Your return URL
    if url.host == "checkout.merchant.com", url.path.hasPrefix("/pay/return") {
        onPaymentReturn(url); return decisionHandler(.cancel)
    }
    // 1. Non-http(s) schemes belong to the OS
    if !["http", "https"].contains(url.scheme ?? "") {
        UIApplication.shared.open(url); return decisionHandler(.cancel)
    }
    // 2. The bank's domain: try the bank app first, then load the page
    if !(url.host ?? "").hasSuffix("fintecture.com") {
        UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { opened in
            if !opened { webView.load(URLRequest(url: url)) }
        }
        return decisionHandler(.cancel)
    }
    if action.targetFrame == nil { webView.load(action.request) } // window.open
    decisionHandler(.allow)
}
<WebView
  source={{ uri: connectUrl }}
  sharedCookiesEnabled
  thirdPartyCookiesEnabled
  setSupportMultipleWindows={false}
  onShouldStartLoadWithRequest={(req) => {
    const u = new URL(req.url);
    if (u.hostname === 'checkout.merchant.com' && u.pathname.startsWith('/pay/return')) {
      onPaymentReturn(u); return false;
    }
    if (!/^https?:$/.test(u.protocol) || !u.hostname.endsWith('fintecture.com')) {
      Linking.openURL(req.url); return false;
    }
    return true;
  }}
/>
NavigationDelegate(onNavigationRequest: (req) {
  final u = Uri.parse(req.url);
  if (u.host == 'checkout.merchant.com' && u.path.startsWith('/pay/return')) {
    onPaymentReturn(u); return NavigationDecision.prevent;
  }
  if (!u.scheme.startsWith('http') || !u.host.endsWith('fintecture.com')) {
    launchUrl(u, mode: LaunchMode.externalApplication);
    return NavigationDecision.prevent;
  }
  return NavigationDecision.navigate;
})

Also: keep the platform's default user agent, use the default persistent cookie store, and implement WKUIDelegate.createWebViewWith (iOS) or setSupportMultipleWindows with onCreateWindow (Android) if a bank uses window.open.

Return to your app

ParameterRule
redirect_urihttps only; custom schemes are rejected. Own it as an App Link / Universal Link and intercept it in the webview. Connect appends session_id, status and state.
origin_uriReturn on cancel. Same rules as redirect_uri.
stateYour order reference. Use it to reopen the right order after a restart and to refuse a second payment for an order already paid.
status query parameterA hint only. Confirm with a webhook or GET the payment before releasing the order.

🚧 Important

Never confirm an order from the redirect URL alone. Webhooks are the source of truth; treat repeated or out-of-order notifications idempotently. On resume, re-read the existing payment: do not create a new session.

Do not

  • Load Connect in an iframe: bank pages refuse framing and the flow dies at the bank.
  • Override the webview user agent: banks and Connect use it to recognise the container.
  • Rely on a JavaScript timer in a backgrounded webview to detect the result.

Test before going live

Sandbox banks do not open apps, so the handoff cannot be tested in SANDBOX. Test in PRODUCTION with a low amount, on Android and iOS, with the bank app installed and not installed, with your app in background and killed during the payment, and with a cancellation at the bank. Banks that require their app on mobile (for example KBC or ING in Belgium) are a good stress test.


Did this page help you?