Instant Bank Transfer

Instant Bank Transfer lets customers pay directly from their bank account — no card required. Powered by open banking, it offers strong security through bank-level authentication and settles account-to-account, making it a popular low-cost payment option across Europe.

To authorize the payment, the customer is briefly taken to their banking app or browser and then returned to your app via a deep link — this page covers the full setup. The same deep link configuration also applies when Instant Bank Transfer is offered through the Hosted Payment Page.

Order pageInstant Bank Page

Flow: user taps Instant bank transfer → SDK opens the banking flow → user authorizes in their bank app or browser → user is redirected back to your app via deep link → result is returned via PBLPaymentResult.

Prerequisites

  • The SDK is added to your project — see iOS SDK.
  • Your /mobile/init request includes the redirect URLs (url_success, url_failed, url_return, notification_url) and app_schema. These are required for Instant Bank Transfer — see Setup Your Server.
  • Your app registers the URL scheme used in those redirect URLs (see below).

Deep link configuration (Info.plist)

Register the URL scheme so the bank redirect returns to your app. The scheme must match the app_schema and redirect URLs you send in /mobile/init. Add this to your Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>payabl</string>
    </array>
    <key>CFBundleURLName</key>
    <string>Payment Callback</string>
  </dict>
</array>

The redirect URLs sent in /mobile/init can use your custom scheme or Universal Links:

{
  "app_schema": "payabl",
  "url_success": "payabl://payment-callback/success",
  "url_failed": "payabl://payment-callback/failed",
  "url_return": "https://yourdomain.com/payment-callback/return",
  "notification_url": "https://your-server.com/api/webhook"
}

Integration

1. Fetch session configuration

Request a session from your backend and build a PBLConfiguration. Create a new session for every payment attempt.

class DemoCartViewModel: ObservableObject {
    // In your server, this endpoint should call the /mobile/init API
    let backendCheckoutUrl = URL(string: "backend_endpoint/payment_page")!
    var instantBankTransfer: PBLInstantBankTransfer?
    var configuration: PBLConfiguration?
    @Published var pblInstantBankTransferLoaded: Bool = false

    func prepareOrder() async -> PBLConfiguration? {
        var request = URLRequest(url: backendCheckoutUrl)
        request.httpMethod = "POST"

        await withCheckedContinuation { continuation in
            let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
                guard let data = data,
                      let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
                else { return }

                let sessionId = json["sessionId"] as! String
                let ephemeralKey = json["empheralKey"] as! String
                let transactionId = json["transactionId"] as! Int

                self?.configuration = PBLConfiguration(
                    sessionId: sessionId,
                    ephemeralKey: ephemeralKey,
                    customerId: "", // not used for Instant Bank Transfer
                    environment: .sandbox, // use .production when going live
                    transactionId: transactionId,
                    appleMerchantId: "merchant.payabl.isdk" // your Apple Merchant ID
                )
                continuation.resume()
            }
            task.resume()
        }
        return self.configuration
    }

    func prepareInstantBankTransfer() async -> PBLConfiguration? {
        guard await prepareOrder() != nil else { return nil }
        return self.configuration
    }
}

2. Adopt the delegates

Adopt PBLInstantBankTransferDelegate to provide the configuration when the button is tapped, receive the result, and supply your redirect path. Adopt PBLErrorDelegate for SDK errors raised outside the payment result.

extension DemoCartViewModel: PBLInstantBankTransferDelegate {
    // Called when the user taps the button — load the session, then hand the config back to the SDK
    func instantBankTransferTapped(onLoadSessionCompleted: @escaping (PBLConfiguration) -> Void) {
        Task {
            guard let config = await prepareInstantBankTransfer() else { return }
            onLoadSessionCompleted(config)
        }
    }

    // Called with the final result of the payment
    func instantBankTransferCompleted(result: PBLPaymentResult) {
        handleInstantBankResult(result)
    }

    // Return your redirect path — must match the redirect URLs sent in /mobile/init
    func instantBankTransferRedirectionUrl() -> String {
        return "payabl://payment-callback"
    }
}

extension DemoCartViewModel: PBLErrorDelegate {
    func errorOccured(error: any Error) {
        // Handle SDK errors (e.g., configuration or session issues)
        print("error from sdk: ", error)
    }
}

3. Add the button

Create the PBLInstantBankTransfer component and place its button in your view.

struct ContentView: View {
    let cartVM = DemoCartViewModel()

    var body: some View {
        HStack {
            if cartVM.pblInstantBankTransferLoaded {
                cartVM.instantBankTransfer?.button {
                    Text("Instant bank transfer")
                        .foregroundStyle(.white)
                        .background { Color.green }
                }
            }
        }
        .onAppear {
            cartVM.instantBankTransfer = PBLInstantBankTransfer(delegate: cartVM, errorDelegate: cartVM)
            cartVM.pblInstantBankTransferLoaded = true
        }
    }
}

4. Handle the result

instantBankTransferCompleted returns a PBLPaymentResult. Handle all three cases.

func handleInstantBankResult(result: PBLPaymentResult) {
    switch result {
    case .completed:
        // Payment flow finished — confirm via webhook before fulfilling (see note below)
        break
    case .canceled:
        // User cancelled the transfer
        break
    case .failed:
        // Inspect the error code — see Error Code Reference
        break
    }
}
📘

Confirm the final status server-side

A completed result means the payment flow finished — it does not guarantee a successful transaction. The authoritative status is confirmed server-to-server via the notification_url webhook. Reconcile against your backend before fulfilling the order. This applies whether Instant Bank Transfer is used standalone or through the Hosted Payment Page.

Testing

Use the sandbox environment (.sandbox) and the bank simulator to exercise success, failure, and cancellation returns. For sandbox bank values and test data across all payment methods, see Testing.

💡

Test your URL scheme registration directly from Terminal with the Simulator running, without a full payment:

xcrun simctl openurl booted "payabl://payment-callback/success"

Common issues

IssueCause & resolution
App doesn't return after authorizing in the bankURL scheme not registered in Info.plist, or it doesn't match the app_schema / redirect URLs sent in /mobile/init.
Bank flow opens but the result never arrivesinstantBankTransferRedirectionUrl() doesn't match the redirect URLs configured in /mobile/init.
Backend error at initMissing app_schema or redirect URLs in /mobile/init. See Setup Your Server.

Integration checklist

Before going live:

  • SDK installed per the repository README; PBLConfiguration uses .production in production builds.
  • Backend creates a session (session_id, ephemeral_key) and sends url_return, url_success, url_failed, notification_url, and app_schema.
  • Info.plist registers the URL scheme matching your redirect URLs.
  • instantBankTransferRedirectionUrl() returns the correct redirect path.
  • URL scheme tested via xcrun simctl openurl.
  • Completed, Canceled, and Failed cases all handled; success is only confirmed after the notification_url webhook.

When something goes wrong, see Error Handling & Troubleshooting.