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)
}
// Usage
launchScanToPayWallet(this, "0123456789", "masterpass.absa.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. Three 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(
"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.absa.scheme" />
</intent>
<!-- repeat for each wallet -->
</queries>Strategy 3 — show a chooser
Pass the intent without a specific scheme and let Android show its default chooser. The user picks whichever wallet they have. Cleanest UX if you don't want to maintain the wallet list yourself.
Receive the return
When the customer finishes in the wallet app, it opens your your.app.scheme://merchant.com?status=SUCCESS&transactionId=... 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 transactionId = data.getQueryParameter("transactionId")
handlePaymentReturn(status, transactionId)
}
}
}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
