App-to-app overview
The full app-to-app flow, the per-bank URL scheme convention, and the trust model that keeps your goods safe.
App-to-app uses URL schemes to hand the customer from your merchant app to their Scan to Pay-enabled wallet app, and back. Both Android and iOS support the pattern; the implementation details differ but the concept is identical.
The flow
Customer in your app Your backend Scan to Pay Wallet app on phone
────────────────── ──────────── ─────────── ──────────────────
│ │ │ │
│ taps "Pay" │ │ │
├───────────────────────────►│ │ │
│ │ POST /code/create │ │
│ ├───────────────────►│ │
│ │ │ │
│ │◄──── code ─────────┤ │
│ │ │
│◄────── code ───────────────┤ │
│ │
│ ┌─ launch wallet via its registered scheme / universal link ──────►│
│ │ │
│ │ [customer authorises] │
│ │ │
│ │ │ webhook (CB) │ │
│ │ │◄────────────────┤ │
│ │ │ ack 200 │ │
│ │ ├────────────────►│ │
│ │ │
│◄─┴──── return via your.app.scheme://?status=SUCCESS&ref=... ─────────┤
│ │
│ verify with your backend before releasing goods │
├────────────────────────────►│ │
Eight steps:
- Customer taps "Pay" in your app.
- Your app calls your backend to start the payment.
- Your backend calls
POST /code/createon the Scan to Pay API (same endpoint as Dynamic QR — see Dynamic QR). - The platform returns a 10-digit code.
- Your app launches the wallet via its registered URL scheme (or FNB universal link) containing the code and a return URL.
- The customer authorises in the wallet app with PIN or 3DS.
- Scan to Pay sends a webhook to your backend with the outcome.
- The wallet app returns to your app via your custom URL scheme, with status as a query parameter.
Wallet launch reference
Most wallet apps register a URL scheme using the historical masterpass. prefix. The standalone Scan to Pay app has different Android and iOS registrations, so choose the scheme for both the wallet and the platform you intend to launch.
| Wallet | Platform | Launch method |
|---|---|---|
| Scan to Pay standalone app | Android | masterpass.app.scheme:// |
| Scan to Pay standalone app | iOS | masterpass.capitec.scheme:// |
| ABSA | Android / iOS | masterpass.absa.scheme:// |
| Standard Bank | Android / iOS | masterpass.sbsa.scheme:// |
| Nedbank | Android / iOS | masterpass.nedbank.scheme:// |
| Capitec-enabled wallet | Android / iOS | masterpass.capitec.scheme:// |
| VodaPay | Android / iOS | masterpass.vodapay.scheme:// |
| Spenda | Android / iOS | masterpass.spenda.scheme:// |
| FNB / RMB | Android / iOS | HTTPS universal link — see FNB / RMB universal link |
Historical naming. The standalone iOS app retains themasterpass.capitec.schemeregistration, which is also the published Capitec-compatible scheme, while the standalone Android app usesmasterpass.app.scheme. Do not substitutescantopay://,masterpass://, or another inferred scheme; those values do not identify the standalone app.
The URL you build looks like:
{wallet scheme}://masterpass.oltio.co.za/{10-digit code}/{URL-encoded return URL}
Where:
{10-digit code}is the value returned fromPOST /code/create{URL-encoded return URL}is your own app's URL scheme — what the wallet will open after the customer finishes. Must be URL-encoded.
For example, to launch the standalone Scan to Pay app:
masterpass.app.scheme://masterpass.oltio.co.za/0123456789/your.app.scheme%3A%2F%2Fmerchant.commasterpass.capitec.scheme://masterpass.oltio.co.za/0123456789/your.app.scheme%3A%2F%2Fmerchant.comFNB / RMB universal link
FNB and RMB use an HTTPS universal link instead of the wallet-scheme path above:
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. Keep the parameter names exactly as shown.
The trust model
Never release goods on the wallet's return alone.The wallet returns to your app via a URL scheme with
status=SUCCESS(or another value) and a transaction reference in the query string. An attacker can call your URL scheme manually with a forged status and reference. Treat the return as a hint that the customer's flow finished, not as proof of payment.Always verify via your backend webhook (or
getTransactionStateif you don't have webhooks configured) before treating an order as paid.
The reliable pattern:
- The wallet returns to your app — your UI moves to a "Confirming…" state.
- Your app polls your backend for the order's status.
- Your backend already received (or will shortly receive) the encrypted webhook with the authoritative outcome.
- Once your backend confirms
SUCCESS, your UI shows the success page.
The wallet's return is the trigger for your UI to update; the webhook is the proof of payment.
What's next
- Implement on Android → App-to-app on Android
- Implement on iOS → App-to-app on iOS
- Parse the response when the wallet returns → Handling the response
- Receive the authoritative outcome via webhook → Webhooks
- Same code-create endpoint as dynamic QR → Dynamic QR
Updated 4 days ago
