App-to-app on iOS

Launch the Scan to Pay wallet from your iOS app via URL scheme and receive the return via your registered scheme.

On iOS, app-to-app uses UIApplication.shared.open(_:) to launch the wallet via its custom URL scheme. You build the URL with the wallet's scheme + the 10-digit code + a URL-encoded return URL, and iOS routes it to the wallet app.


Prerequisites

1. Declare wallet schemes in Info.plist

iOS requires you to declare every URL scheme your app might open. Add a LSApplicationQueriesSchemes entry to your Info.plist listing all the wallet schemes you want to support:

<key>LSApplicationQueriesSchemes</key>
<array>
  <!-- Standalone Scan to Pay app (historical iOS registration) -->
  <string>masterpass.capitec.scheme</string>
  <string>masterpass.absa.scheme</string>
  <string>masterpass.sbsa.scheme</string>
  <string>masterpass.nedbank.scheme</string>
  <string>masterpass.vodapay.scheme</string>
  <string>masterpass.spenda.scheme</string>
  <string>nedbank</string>
</array>

Without this list, UIApplication.canOpenURL(_:) returns false for any wallet scheme and your launch attempt will silently fail.

FNB / RMB use an HTTPS universal link, not a custom scheme. Do not put the complete FNB URL in LSApplicationQueriesSchemes; open the universal link directly as shown below.

2. Register your own URL scheme

Define a scheme that the wallet uses to return to your app. Add CFBundleURLTypes:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.merchant.example</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>your.app.scheme</string>
    </array>
  </dict>
</array>

3. Your backend creates a code

via POST /code/create — see Dynamic QR for the request shape. Your iOS app receives the 10-digit code from your backend.


Launch the wallet

import UIKit

func launchScanToPayWallet(code: String, walletScheme: String) {
    let returnUrl = "your.app.scheme://merchant.com"
    let unreserved = CharacterSet(
        charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
    )

    guard let encodedReturn = returnUrl.addingPercentEncoding(
              withAllowedCharacters: unreserved) else {
        return
    }

    let urlString = "\(walletScheme)://masterpass.oltio.co.za/\(code)/\(encodedReturn)"
    guard let url = URL(string: urlString) else { return }

    if UIApplication.shared.canOpenURL(url) {
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    } else {
        // Wallet not installed — fall back to QR display, or prompt to install
    }
}

// Standalone Scan to Pay app on iOS
launchScanToPayWallet(code: "0123456789", walletScheme: "masterpass.capitec.scheme")
- (void)launchScanToPayWalletWithCode:(NSString *)code walletScheme:(NSString *)walletScheme {
    NSString *returnUrl = @"your.app.scheme://merchant.com";
    NSString *encodedReturn = [returnUrl
        stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]];

    NSString *urlString = [NSString stringWithFormat:@"%@://masterpass.oltio.co.za/%@/%@",
                          walletScheme, code, encodedReturn];
    NSURL *url = [NSURL URLWithString:urlString];

    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
    }
}

Three things to get right:

DetailWhy it matters
URL-encode the return URLWithout it, the : and / in your scheme break the wallet's URL parser.
Check canOpenURL firstReturns false if the wallet isn't installed or you didn't declare the scheme in LSApplicationQueriesSchemes.
Declare every wallet scheme you want to queryApple's privacy policy limits canOpenURL to declared schemes from iOS 9 onwards.

Picking the right wallet

📘

iOS does not show a system wallet chooser. If you support more than one wallet, present a wallet picker in your own app and launch the selected wallet's specific scheme.

Use canOpenURL on each scheme to hide or disable wallets the customer doesn't have installed.

let wallets: [(name: String, scheme: String)] = [
    ("Scan to Pay / Capitec", "masterpass.capitec.scheme"),
    ("ABSA",          "masterpass.absa.scheme"),
    ("Standard Bank", "masterpass.sbsa.scheme"),
    ("Nedbank",       "masterpass.nedbank.scheme"),
    ("VodaPay",       "masterpass.vodapay.scheme"),
    ("Spenda",        "masterpass.spenda.scheme")
]

let installed = wallets.filter { wallet in
    UIApplication.shared.canOpenURL(URL(string: "\(wallet.scheme)://test")!)
}

Show only the installed wallets to keep your picker clean.

FNB / RMB universal link

FNB and RMB use a universal link rather than a custom scheme. 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 parameterRequired value
codetypeThe fixed value masterpassdeeplink
urlencodedqrThe URL-encoded 10-digit code returned by POST /code/create
urlencodedcallbackurlYour 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.com

Percent-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, the wallet opens your.app.scheme://merchant.com?status=SUCCESS&ref=.... iOS dispatches this to your app's URL handler:

@main
struct MerchantApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    handleScanToPayReturn(url: url)
                }
        }
    }
}

func handleScanToPayReturn(url: URL) {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let queryItems = components.queryItems else { return }

    let status = queryItems.first(where: { $0.name == "status" })?.value
    let ref    = queryItems.first(where: { $0.name == "ref" })?.value

    // Use your own order context plus ref to ask your backend for the authoritative outcome.
    // Never release goods based on this return URL alone.
}
func application(_ app: UIApplication, open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    handleScanToPayReturn(url: url)
    return true
}

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


Did this page help you?