Skip to content

Mobile apps ​

A mobile app connects a bank through the same hosted connect flow as a web app. There is no mobile SDK to install: the app opens one URL in the system browser and catches one redirect back. Everything that needs the API key happens on your server.

How it fits together ​

  1. The app asks your backend for a connect session (your own endpoint).
  2. Your backend calls POST /v2/connect-sessions with the API key and the returnUrl for this user, and hands only the hostedUrl to the app.
  3. The app opens hostedUrl in a system browser session (below).
  4. The flow ends with a redirect to your returnUrl, which the operating system routes back into your app.
  5. The app tells your backend the flow ended; your backend reads the authoritative result from GET /v2/connect-sessions/{sessionId} and the webhooks.
sh
# Your backend, step 2
curl -X POST https://aisp-partner.bbapi.io/v2/connect-sessions \
  -H "X-Api-Key: {{BB_API_KEY}}" \
  -H "X-Client-Id: <clientId>" \
  -H "Content-Type: application/json" \
  -d '{ "returnUrl": "https://app.example.com/bb-callback" }'

The API key never ships in the app. An app binary can be unpacked, and a key in it is a key anyone can use. See Authentication.

Never a WebView ​

Open hostedUrl with ASWebAuthenticationSession on iOS and Custom Tabs on Android, never in an embedded WKWebView or android.webkit.WebView. Native apps must not use embedded user agents for authorization requests (RFC 8252, section 8.12): the app could read what the user types, and the user cannot see the address bar to check they are really at their bank. Many banks detect embedded views and refuse to load their login page.

The return URL ​

Register the return URL for your app in the portal (Apps → return URLs) before the first run; a session whose returnUrl matches no registered pattern is rejected with validation_error.

  • Universal link (iOS) / app link (Android), for example https://app.example.com/bb-callback: recommended. The operating system verifies that your app owns the domain, so no other app can catch the redirect. Serve apple-app-site-association and assetlinks.json from your production domain.
  • Custom scheme, for example acmewallet://bb-callback: works without a domain, but any app can claim a scheme. Use it as a fallback only.

The redirect appends these query parameters to your returnUrl:

ParameterPresenceMeaning
sessionIdalwaysthe connect session this result belongs to
resultCodealwaysOk, Error or Cancelled (exact case)
connectionIdoptionalset when a connection was created
erroroptionalmachine-readable failure reason when resultCode is Error

Parsing rules:

  • A URL is a return link only when it has a non-empty sessionId and a resultCode that is exactly one of the three values. Ignore anything else.
  • If a parameter repeats, the first occurrence wins.
  • Your own query parameters on the returnUrl stay in place; ignore them when parsing.
  • sessionId, connectionId and hostedUrl are opaque strings. Never parse their contents.
  • The redirect is a UX signal, not the result. The user may close the browser before it arrives, so confirm the outcome from your backend in every case, including when no redirect came.

iOS ​

ASWebAuthenticationSession shows the flow in a browser sheet and catches the redirect itself. A redirect to a universal link inside that sheet does not reliably reopen your app, so tell the session which return URL to catch:

  • iOS 17.4+: ASWebAuthenticationSession.Callback.https(host:path:) for your universal-link returnUrl. The app needs the Associated Domains entitlement with webcredentials:app.example.com for that host, and your apple-app-site-association file needs a webcredentials entry next to applinks.
  • Older iOS: a custom scheme (callbackURLScheme), registered in the portal as a second return URL.
swift
import AuthenticationServices

final class ConnectFlow: NSObject, ASWebAuthenticationPresentationContextProviding {
    private var session: ASWebAuthenticationSession?
    private weak var anchor: ASPresentationAnchor?

    // anchor: the presenting scene's key window, e.g. view.window.
    func start(hostedUrl: URL, anchor: ASPresentationAnchor, onReturn: @escaping (URL?) -> Void) {
        self.anchor = anchor
        let done: ASWebAuthenticationSession.CompletionHandler = { url, _ in
            onReturn(url) // nil when the user closed the sheet
        }
        if #available(iOS 17.4, *) {
            session = ASWebAuthenticationSession(
                url: hostedUrl, callback: .https(host: "app.example.com", path: "/bb-callback"),
                completionHandler: done)
        } else {
            session = ASWebAuthenticationSession(url: hostedUrl, callbackURLScheme: "acmewallet", completionHandler: done)
        }
        session?.presentationContextProvider = self
        session?.start()
    }

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        anchor ?? ASPresentationAnchor()
    }
}

The returnUrl your backend sends must match what this device catches: the https one on iOS 17.4+, the custom scheme below that. Read sessionId from the returned URL with URLComponents per the rules above, or keep the one your backend returned, and ask your backend for the result. A nil URL means the user closed the sheet: treat it as cancelled, and still let your backend check the session.

Android ​

Custom Tabs (androidx.browser:browser) open the flow in the user's browser. The return link reopens your activity through its intent filter. Declare the activity singleTask: the Custom Tab sits above it in your app's task, so singleTop would start a second instance, while singleTask delivers the link to the existing one through onNewIntent and closes the tab.

xml
<activity android:name=".ConnectActivity" android:launchMode="singleTask">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="app.example.com" android:path="/bb-callback" />
    </intent-filter>
</activity>
kotlin
private var flowOpen = false

fun startConnect(hostedUrl: String) {
    flowOpen = true
    CustomTabsIntent.Builder().build().launchUrl(this, Uri.parse(hostedUrl))
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    intent.data?.let { flowOpen = false; onReturn(it) } // parse per the rules above
}

override fun onResume() {
    super.onResume()
    // Custom Tabs have no close callback: back in the app without a return link
    // (onNewIntent runs first) means the user closed the tab.
    if (flowOpen) { flowOpen = false; onClosed() }
}

Handle a return that starts the app cold in onCreate the same way. For a custom scheme, use the same intent filter with android:scheme="acmewallet" and no autoVerify.

Checklist ​

  • The app never holds an API key; your backend creates every session.
  • hostedUrl is opened once, in ASWebAuthenticationSession or a Custom Tab.
  • The return URL is registered in the portal and verified as a universal link / app link on your production domain.
  • Closing the browser mid-flow lands in your cancelled path, not a hang.
  • Your backend confirms the result from the session state and webhooks.