BlinkPay supports mobile applications on iOS and Android. This guide covers the architecture a mobile integration needs, how to open the bank's pages from your app, how to bring the customer back to your app, and how to find out the outcome reliably.
Embedded WebViews and iFrames Not Supported
Don't show BlinkPay or bank pages in an embedded WebView (WKWebView on iOS, WebView on Android) or an iframe. Use SFSafariViewController or ASWebAuthenticationSession on iOS and Chrome Custom Tabs on Android, as described in Opening the Bank Pages.
Use the Gateway Flow with an in-app browser and a registered return link. The gateway handles bank selection, flow routing and platform detection, and on mobile it opens the customer's banking app where the bank supports it. When the customer has finished, it sends them back to your app through the return link you register (see Register Your Return Link).
A mobile app needs a backend server of your own. Your client ID and client secret live only on that backend, which requests access tokens and makes every BlinkPay API call; they must never be compiled into the app, where anyone can extract them. The backend also decides the amount and the payment reference details (PCR) from its own record of the order, rather than accepting them from the app, because a modified app could send any amount it likes.
The app authenticates to your backend in the same way as the rest of your app does, with your own user sessions or login tokens. It never holds BlinkPay credentials or BlinkPay access tokens. See Credential and Integration Security for how to store and rotate the credentials on the backend.
redirect_uri from the response to the app.cid (the consent ID; for a quick payment, the quick_payment_id).Mobile App → Your Backend → BlinkPay API
↓
redirect_uri returned to app
↓
SFSafariViewController / Chrome Custom Tabs → Gateway → Banking App
↓
Return link (cid) → Mobile App → Your Backend → GET the consent
The BlinkPay Flutter Demo is a complete working example. It is a reference implementation: with a little help from an AI assistant it translates readily to other languages and frameworks, so use it as the reference whatever you build in. It uses the gateway flow with single consents (one-off payments) and enduring consents (recurring payments). Its server/ folder is a small backend proxy that holds the client credentials and obtains the access tokens, so the app contains no BlinkPay secrets. The app opens the gateway with flutter_custom_tabs (Chrome Custom Tabs on Android, SFSafariViewController on iOS) and receives the custom-scheme return link blinkpaydemo://callback with app_links. Its README's Deep Linking Setup section shows the Android manifest, the iOS Info.plist entries and the iOS code that closes the in-app browser.
Reference Implementation Only
The Flutter Demo is a reference implementation, not a supported SDK package. It takes shortcuts that keep the demo small and must not be copied into a real app: the app sets the amount and PCR and the proxy passes them through unchecked (your backend must set them); the app authenticates to the proxy with a fixed demo API key built into the app (use your own user sessions); and the consent ID is held only in the app's memory (store it on your backend, so that the outcome can still be found if the app is closed).
Open the gateway, or the bank's authorisation page in the redirect flow, in the platform's in-app browser: SFSafariViewController or ASWebAuthenticationSession on iOS, and Chrome Custom Tabs on Android. These run the page in a real browser that your app cannot read or script, so the customer's banking credentials never pass through your app. Banks' anti-framing policies block their pages from being embedded, and in a real browser the customer can see the bank's genuine address before signing in.
| Platform | In-app browser (examples) | Notes |
|---|---|---|
| iOS | SFSafariViewController, or ASWebAuthenticationSession | SFSafariViewController does not close itself when your return link arrives; dismiss it yourself |
| Android | Chrome Custom Tabs (AndroidX Browser library) | Falls back to the default browser on devices without a Custom Tabs browser |
| Flutter | flutter_custom_tabs (used by the Flutter Demo) | Pair it with a link handler such as app_links |
| React Native | react-native-inappbrowser-reborn (InAppBrowser.open, or openAuth for its auth-session mode), or Expo's expo-web-browser (openBrowserAsync, or openAuthSessionAsync) | open and openBrowserAsync use SFSafariViewController and Custom Tabs; close them with InAppBrowser.close or dismissBrowser (iOS) when your return link arrives |
These packages are examples, not endorsements; any library that uses the platform's in-app browser works. The auth-session modes (ASWebAuthenticationSession, openAuth, openAuthSessionAsync) close themselves when the return link arrives, but on iOS they first show a system prompt saying that your app wants to use the website to sign in, which can confuse a customer who is paying rather than signing in. SFSafariViewController and its equivalents show no such prompt.
Your return link is the redirect_uri you send when you create a consent or quick payment with the gateway or redirect flow. On mobile it is a link that opens your app. Two kinds work:
| Kind | Example | Notes |
|---|---|---|
| Custom scheme | myapp://callback | Simple to set up, and what the Flutter Demo uses. Must include a host part: myapp:callback is rejected. Any app can claim the same scheme, which is one more reason to confirm every outcome on your backend |
| Universal link (iOS) / App Link (Android) | https://app.example.com/blinkpay/return | An HTTPS link verified as belonging to your app, which needs the apple-app-site-association (iOS) and assetlinks.json (Android) files hosted on that domain; opens a web page of yours if the app is not installed. Test it on a real device, because these links don't always open the app when they are reached through a redirect rather than a tap |
Register the link yourself in the portal: under Settings > API Settings, open the environment's row and add it under Whitelisted Redirect URLs. Users with the Administrator or Developer role can do this. Sandbox and production have separate lists, so register the link in each environment you use. Custom schemes are accepted in both environments; production rejects http:// and localhost links, which sandbox allows. Banks do not need to whitelist your return link, and you don't need to contact support for it. Support is needed only for App-to-App returns, where the bank's app opens your app directly.
The redirect_uri you send must match a registered link, otherwise the request fails with 400 (BP214 for Blink Payments, BDT211 for Blink Data). Query strings are ignored, so you can add your own, such as ?order=123. See Redirect URI Whitelisting for the matching rules.
Your backend must know which consent or quick payment belongs to which order before the customer leaves your app, so store the quick_payment_id or consent ID on your backend as soon as you create it and before the app opens the browser. Keeping it only in the app is not enough: the operating system can close your app while the customer is in their bank's app.
When your return link arrives, check that it is your scheme, host and path, and that its cid matches the ID you stored for the order the customer is paying for; ignore a stale or unknown one. Branch on its status parameter, never on the error text, and treat the link as a hint only: it is not proof of payment, because it can be forged or never arrive. Return Parameters lists what it can carry. Then ask your backend for the outcome, and have the backend GET the consent or quick payment from the BlinkPay API. Close the in-app browser at this point (see the Platform Checklist).
The return link often never arrives. Closing SFSafariViewController or a Custom Tab, pressing the back button or switching away from the app fires no redirect, and a customer who approves in their banking app may come back to your app by hand. So whenever your app returns to the foreground while a payment is in progress, ask your backend for the current status rather than waiting for the link.
Reconcile; don't assume failure. If the consent is still AwaitingAuthorisation, the customer may still approve in their banking app, so don't revoke it or treat it as failed just because they came back to your app. Poll it through your backend every second until it reaches a final status. A consent that is never authorised ends by itself when the window passes. For Blink Payments, once a payment exists, check it every second until it is AcceptedSettlementCompleted or Rejected. Stop polling at a final status; polling the same consent or payment hundreds of times does not make it finish sooner and can get your requests rate limited.
Blink Payments only: for a quick payment through the gateway or redirect flow, have your backend call GET /payments/v1/quick-payments/{id} as soon as the return link arrives or the app resumes; the gateway normally does this for you, and calling it again is safe. For a bank payment that call completes the payment. Run a backend sweep over stored quick payments that never got a return, and read each outcome from consent.payments[0].status.
While the outcome is not final, tell the customer that you are checking with their bank rather than showing a failure, and don't let them pay for the same order again until the first attempt has a final status, otherwise they may pay twice. If you stop waiting before it is final, show the order as awaiting confirmation, give the customer a way to check again later, and keep checking on your backend.
AppDelegate.swift); the auth-session modes close themselves.CFBundleURLTypes in Info.plist; a universal link needs the Associated Domains entitlement and an apple-app-site-association file on your domain.VIEW, DEFAULT and BROWSABLE and your scheme and host to the activity that handles the return, or an autoVerify HTTPS filter for an App Link, with the matching assetlinks.json file on your domain.singleTop for Flutter, singleTask for React Native) and avoid singleInstance.<queries> entry for VIEW intents with the https scheme so your app can find a browser to open the Custom Tab. Add bank apps' package names only if your app checks whether a bank app is installed.10.0.2.2, not localhost; the iOS Simulator can use localhost; a physical device needs your machine's network address or a tunnel. This applies to your backend URL; the return link is your app's own link and needs no network address. Bank apps are not available on emulators, but the sandbox test bank (PNZ) uses the browser, so the whole sandbox flow can be tested there.