App-to-app on Android
Launch the Scan to Pay wallet from your Android app via Intent and receive the return via your registered URL scheme.
On Android, app-to-app uses standard Intent.ACTION_VIEW with a URL. You build the URL with the wallet's scheme + the 10-digit code + a URL-encoded return URL, and Android's intent system routes it to the wallet app.
Prerequisites
-
Register your own URL scheme in your
AndroidManifest.xml. This is what the wallet uses to return to your app.<activity android:name=".PaymentReturnActivity" android:exported="true"> <intent-filter android:label="Return from Scan to Pay"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="your.app.scheme" android:host="merchant.com" /> </intent-filter> </activity>Replace
your.app.schemeandmerchant.comwith your own values. You'll receive the wallet's return via this activity. -
Your backend creates a code via
POST /code/create— see Dynamic QR for the request shape. Your Android app receives the 10-digit code from your backend and uses it to launch the wallet.
Launch the wallet
Use Intent.ACTION_VIEW with the appropriate wallet scheme. This MUST be called from an Activity:
fun launchScanToPayWallet(activity: Activity, code: String, walletScheme: String) {
val returnUrl = URLEncoder.encode("your.app.scheme://merchant.com", "UTF-8")
val deepLink = "$walletScheme://masterpass.oltio.co.za/$code/$returnUrl"
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(deepLink)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
activity.startActivity(intent)
}
// Standalone Scan to Pay app on Android
launchScanToPayWallet(this, "0123456789", "masterpass.app.scheme")public static void launchScanToPayWallet(Activity activity, String code, String walletScheme)
throws UnsupportedEncodingException {
String returnUrl = URLEncoder.encode("your.app.scheme://merchant.com", "UTF-8");
String deepLink = walletScheme + "://masterpass.oltio.co.za/" + code + "/" + returnUrl;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(deepLink));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}Three things to get right:
| Detail | Why it matters |
|---|---|
| URL-encode the return URL | If you skip this, the wallet won't be able to construct a working return URL when the customer finishes. |
Use FLAG_ACTIVITY_NEW_TASK | Otherwise the back stack can become confused when control returns to your app. |
Call from an Activity | Background services can't launch other apps without further plumbing. |
Picking the right wallet scheme
The customer might have any combination of wallets installed. Use one of these strategies:
Strategy 1 — let the customer pick at the start
Show your customer a list of wallet logos in your app, let them pick the one they want, and then launch the corresponding scheme. Maintain the wallet-scheme map in your app's resources.
val walletSchemes = mapOf(
"Scan to Pay" to "masterpass.app.scheme",
"ABSA" to "masterpass.absa.scheme",
"Standard Bank" to "masterpass.sbsa.scheme",
"Nedbank" to "masterpass.nedbank.scheme",
"Capitec" to "masterpass.capitec.scheme",
"VodaPay" to "masterpass.vodapay.scheme",
"Spenda" to "masterpass.spenda.scheme"
)Strategy 2 — query installed packages
Use PackageManager.queryIntentActivities to check which wallets are actually installed before showing the picker:
fun installedWallets(context: Context): List<String> {
val pm = context.packageManager
return walletSchemes.filter { (_, scheme) ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$scheme://test"))
pm.queryIntentActivities(intent, 0).isNotEmpty()
}.keys.toList()
}Note: this requires the wallet schemes to be declared in <queries> in AndroidManifest.xml from Android 11 (API 30):
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="masterpass.app.scheme" />
</intent>
<!-- repeat for each wallet -->
</queries>FNB / RMB universal link
FNB and RMB use an HTTPS universal link instead of the wallet-scheme path. Supply the payment code and return URL using the exact query parameters below:
https://www.online.fnb.co.za/banking/mobileservices?codetype=masterpassdeeplink&urlencodedqr={URL-encoded 10-digit code}&urlencodedcallbackurl={URL-encoded return URL}| Query parameter | Required value |
|---|---|
codetype | The fixed value masterpassdeeplink |
urlencodedqr | The URL-encoded 10-digit code returned by POST /code/create |
urlencodedcallbackurl | Your app's URL-encoded return URL |
Example:
https://www.online.fnb.co.za/banking/mobileservices?codetype=masterpassdeeplink&urlencodedqr=0123456789&urlencodedcallbackurl=your.app.scheme%3A%2F%2Fmerchant.comPercent-encode each value once. Detect FNB / RMB separately and open this HTTPS URL when the customer selects either wallet.
Receive the return
When the customer finishes in the wallet app, it opens your your.app.scheme://merchant.com?status=SUCCESS&ref=... URL. Android routes it to your registered activity:
class PaymentReturnActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val data = intent?.data
if (data != null) {
val status = data.getQueryParameter("status")
val ref = data.getQueryParameter("ref")
handlePaymentReturn(status, ref)
}
}
}The status values and what they mean are documented on Handling the response.
The return URL is not proof of payment. It tells you the customer's flow ended; it doesn't tell you the bank approved the transaction. Always verify via your backend webhook before releasing goods. See App-to-app overview.
What's next
- Parse the return status → Handling the response
- Same flow for iOS → App-to-app on iOS
- Receive the authoritative webhook on your backend → Webhooks
- Embed payments inside your app instead of launching the wallet → In-App Payments
- Sandbox test the flow → Sandbox and test cards
Updated 4 days ago
