Loading...
Integration details

Mobile redirects (app-to-app)


Mobile redirects enable payers to authorise payments using their banking app and return to your application afterward. This guide covers how to integrate Volt checkout within a mobile WebView and handle redirect callbacks.

How it works

  1. Your app loads the Volt checkout URL inside a WebView.
  2. The checkout flow triggers redirects (to bank apps or back to merchant).
  3. Your app intercepts these redirects via an injected JavaScript callback.
  4. Your app handles the redirect natively (e.g. opening the bank app, closing the WebView).

Callback contract

To listen to redirect requests from Volt checkout, inject window.voltRedirectCallback as a function on the WebView's window object. Volt checkout will call this function with a JSON payload when a redirect occurs.

Callback payload

Prop

Type

Example payloads:

// Bank redirect — payer is being sent to their banking app
{ "type": "bank_redirect", "url": "https://bank-app.example.com/authorize?token=abc" }

// Merchant redirect — payer cancelled the payment
{ "type": "merchant_redirect", "url": "your-app://checkout/cancel", "result": "cancel" }

// Merchant redirect with payment status
{ "type": "merchant_redirect", "url": "your-app://checkout/success", "result": "success" }

// Merchant redirect with exception status
{ "type": "merchant_redirect", "url": "your-app://checkout/error", "result": "exception" }

Implementation steps

Configure return URLs

Set your payment success/failure URLs to point to views in your mobile app. Use either:

  • App URL schemes: your-app://checkout/success
  • Universal/App Links (HTTPS): https://your-domain.com/checkout/success

Your app must claim these URL schemes in its OS configuration:

Load checkout in a WebView

Load the Volt checkout URL in a WebView and inject the voltRedirectCallback JavaScript function. The injection should happen at document end to ensure the checkout page is ready.

Handle redirects natively

When your callback receives a bank_redirect, open the URL in the system browser or via a deep link to the banking app. When you receive a merchant_redirect, handle the result accordingly (e.g. close the WebView on cancel).

Payer returns to your app

After the payer authenticates in their banking app, they are redirected back to one of your configured payment URLs. Close the WebView and continue the checkout flow in your app.

Platform examples

import SwiftUI
import UIKit
import WebKit

struct CheckoutWebView: UIViewControllerRepresentable {
    typealias UIViewControllerType = CheckoutWebViewController

    func makeUIViewController(context: Context) -> CheckoutWebViewController {
        return CheckoutWebViewController()
    }

    func updateUIViewController(_ uiViewController: CheckoutWebViewController, context: Context) {}
}

class CheckoutWebViewController: UIViewController {
    static let checkoutURL = URL(string: "https://checkout.volt.io/...")!

    static let jsHandlerName = "iOSListener"
    static let injectedJS: String = """
        window.voltRedirectCallback = function(data) {
            window.webkit.messageHandlers.\(jsHandlerName).postMessage(data);
        }
    """

    private lazy var webView: WKWebView = {
        let webView = WKWebView()
        webView.translatesAutoresizingMaskIntoConstraints = false
        return webView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(webView)
        NSLayoutConstraint.activate([
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            webView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
            webView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor)
        ])

        let contentController = webView.configuration.userContentController
        contentController.add(self, name: Self.jsHandlerName)

        let script = WKUserScript(source: Self.injectedJS, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
        contentController.addUserScript(script)

        webView.load(URLRequest(url: Self.checkoutURL))
    }

    func handleRedirect(_ redirectParams: Volt.RedirectParams) {
        switch redirectParams {
        case .bankRedirect(let url):
            guard UIApplication.shared.canOpenURL(url) else {
                print("Error: URL can not be opened")
                return
            }

            UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { success in
                if !success {
                    UIApplication.shared.open(url, options: [:])
                }
            }
        case .merchantRedirect(let merchantRedirectResult, let url):
            if merchantRedirectResult == .cancel {
                // Handle the cancellation without leaving the app
            }
            if merchantRedirectResult == .exception {
                // Handle the exception (e.g. check webhooks)
            }
        }
    }
}

extension CheckoutWebViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard message.name == Self.jsHandlerName else {
            return
        }

        if let messageBody = message.body as? [String: Any] {
            do {
                let jsonData = try JSONSerialization.data(withJSONObject: messageBody, options: [])
                let redirectParams = try JSONDecoder().decode(Volt.RedirectParams.self, from: jsonData)

                handleRedirect(redirectParams)

            } catch {
                print("Error: JSON decoding error: \(error)")
            }
        }
    }
}

enum Volt {
    enum MerchantRedirectResult: String, Decodable {
        case cancel
        case success
        case failure
        case pending
        case exception
    }

    enum RedirectParams: Decodable {
        case bankRedirect(URL)
        case merchantRedirect(MerchantRedirectResult, URL)

        enum CodingKeys: String, CodingKey {
            case type
            case url
            case result
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            guard let type = try container.decodeIfPresent(RedirectType.self, forKey: .type) else {
                throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid value in 'type' field.")
            }
            guard let url = try container.decodeIfPresent(URL.self, forKey: .url) else {
                throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "Invalid value in 'url' field.")
            }
            switch type {
            case .bankRedirect:
                self = .bankRedirect(url)

            case .merchantRedirect:
                if let result = try container.decodeIfPresent(MerchantRedirectResult.self, forKey: .result) {
                    self = .merchantRedirect(result, url)
                } else {
                    throw DecodingError.dataCorruptedError(forKey: .result, in: container, debugDescription: "Invalid value in 'result' field.")
                }
            }
        }
    }

    private enum RedirectType: String, Decodable {
        case bankRedirect = "bank_redirect"
        case merchantRedirect = "merchant_redirect"
    }
}

How is this guide?

Last updated on

On this page