Appearance
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
- The app asks your backend for a connect session (your own endpoint).
- Your backend calls
POST /v2/connect-sessionswith the API key and thereturnUrlfor this user, and hands only thehostedUrlto the app. - The app opens
hostedUrlin a system browser session (below). - The flow ends with a redirect to your
returnUrl, which the operating system routes back into your app. - 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. Serveapple-app-site-associationandassetlinks.jsonfrom 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:
| Parameter | Presence | Meaning |
|---|---|---|
sessionId | always | the connect session this result belongs to |
resultCode | always | Ok, Error or Cancelled (exact case) |
connectionId | optional | set when a connection was created |
error | optional | machine-readable failure reason when resultCode is Error |
Parsing rules:
- A URL is a return link only when it has a non-empty
sessionIdand aresultCodethat is exactly one of the three values. Ignore anything else. - If a parameter repeats, the first occurrence wins.
- Your own query parameters on the
returnUrlstay in place; ignore them when parsing. sessionId,connectionIdandhostedUrlare 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-linkreturnUrl. The app needs the Associated Domains entitlement withwebcredentials:app.example.comfor that host, and yourapple-app-site-associationfile needs awebcredentialsentry next toapplinks. - 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.
hostedUrlis opened once, inASWebAuthenticationSessionor 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.