Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in iOS with Swift

iOS apps cannot call the Paystack API with a secret key. Initialize the transaction on your backend and return the authorization_url. Open it in a WKWebView in Swift. Implement WKNavigationDelegate to detect when Paystack redirects to your callback_url, extract the reference, and verify on your backend using URLSession.

Backend: Initialize Transaction

Call your backend from Swift to get the authorization URL:

// PaymentService.swift
struct InitializeResponse: Codable {
    let authorization_url: String
    let access_code: String
    let reference: String
}

struct InitializeRequest: Codable {
    let email: String
    let amount: Double
}

class PaymentService {
    static let shared = PaymentService()
    private let baseURL = "https://your-backend.com"

    func initialize(email: String, amount: Double) async throws -> InitializeResponse {
        guard let url = URL(string: "(baseURL)/api/pay/initialize") else {
            throw URLError(.badURL)
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(InitializeRequest(email: email, amount: amount))

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        return try JSONDecoder().decode(InitializeResponse.self, from: data)
    }
}

WKWebView Payment Controller

// PaymentViewController.swift
import UIKit
import WebKit

protocol PaymentViewControllerDelegate: AnyObject {
    func paymentDidSucceed(reference: String)
    func paymentDidFail()
    func paymentWasCancelled()
}

class PaymentViewController: UIViewController, WKNavigationDelegate {
    private var webView: WKWebView!
    private let authorizationURL: String
    private let callbackURLPrefix = "https://yourapp.com/payment/callback"

    weak var delegate: PaymentViewControllerDelegate?

    init(authorizationURL: String) {
        self.authorizationURL = authorizationURL
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Complete Payment"
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .cancel,
            target: self, action: #selector(cancel)
        )

        webView = WKWebView(frame: view.bounds)
        webView.navigationDelegate = self
        view.addSubview(webView)

        if let url = URL(string: authorizationURL) {
            webView.load(URLRequest(url: url))
        }
    }

    @objc func cancel() {
        delegate?.paymentWasCancelled()
        dismiss(animated: true)
    }

    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url?.absoluteString ?? ""

        if url.hasPrefix(callbackURLPrefix) {
            // Extract reference from callback URL
            if let components = URLComponents(string: url),
               let reference = components.queryItems?.first(where: {
                   $0.name == "reference" || $0.name == "trxref"
               })?.value {
                decisionHandler(.cancel)
                delegate?.paymentDidSucceed(reference: reference)
                dismiss(animated: true)
                return
            }
        }

        decisionHandler(.allow)
    }
}

SwiftUI Integration

// PaymentWebView.swift (SwiftUI)
import SwiftUI
import WebKit

struct PaystackWebView: UIViewRepresentable {
    let authURL: String
    let callbackPrefix: String
    var onSuccess: (String) -> Void
    var onCancel: () -> Void

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        if let url = URL(string: authURL) {
            webView.load(URLRequest(url: url))
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: PaystackWebView

        init(parent: PaystackWebView) { self.parent = parent }

        func webView(_ webView: WKWebView,
                     decidePolicyFor navigationAction: WKNavigationAction,
                     decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            let url = navigationAction.request.url?.absoluteString ?? ""

            if url.hasPrefix(parent.callbackPrefix),
               let components = URLComponents(string: url),
               let reference = components.queryItems?.first(where: {
                   $0.name == "reference" || $0.name == "trxref"
               })?.value {
                decisionHandler(.cancel)
                parent.onSuccess(reference)
                return
            }
            decisionHandler(.allow)
        }
    }
}

// Usage in a SwiftUI view:
// PaystackWebView(
//     authURL: authURL,
//     callbackPrefix: "https://yourapp.com/payment/callback",
//     onSuccess: { reference in verifyPayment(reference) },
//     onCancel: { dismiss() }
// )

Learn More

Key Takeaways

  • Never put the Paystack secret key in your iOS app bundle. Initialize transactions from a backend server.
  • Use WKWebView with WKNavigationDelegate to display and intercept Paystack checkout.
  • Detect the callback URL in decidePolicyFor navigationAction and extract the reference.
  • Use URLSession (or Alamofire) to call your backend verify endpoint from Swift.
  • SwiftUI apps can use WebView via UIViewRepresentable wrapping WKWebView.
  • Configure App Transport Security to allow HTTPS connections to paystack.co if using older iOS targets.

Frequently Asked Questions

Can I use SFSafariViewController instead of WKWebView for Paystack checkout on iOS?
Yes. SFSafariViewController opens Paystack checkout in an in-app browser with better performance and privacy than WKWebView. To detect the callback, use a universal link or custom URL scheme. The user returns to your app via a deep link. SFSafariViewController cannot intercept navigations like WKWebView can.
Does Paystack have an official iOS Swift SDK?
Paystack does not maintain a well-supported official iOS SDK as of 2026. The WKWebView approach in this guide is reliable and depends only on the stable Paystack REST API. Community libraries exist on GitHub but verify maintenance status before use.
How do I handle App Transport Security (ATS) for Paystack on iOS?
Paystack uses HTTPS on paystack.co, so ATS allows it by default. You should not need to add exceptions. If you see ATS errors, check that your callback URL also uses HTTPS. ATS blocks HTTP connections in iOS apps.
What is the best way to call my backend API from Swift?
URLSession with async/await is the standard library approach. Alamofire is popular for more complex networking needs. For simple POST and GET calls to initialize and verify payments, URLSession is sufficient and has no dependencies.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon