Bonaventure OgetoBy Bonaventure Ogeto|

Verify Paystack Payments in iOS with Swift

After the WKWebView callback delivers the transaction reference, call your backend verify endpoint using URLSession with async/await. Your backend calls the Paystack verify API and returns the result. In Swift, parse the JSON response, check verified == true, and update your SwiftUI view or UIKit label accordingly.

Payment Service: Verify

// PaymentService.swift (continued)
struct VerifyResponse: Codable {
    let verified: Bool
    let amount: Double?
    let currency: String?
    let reference: String?
    let error: String?
}

extension PaymentService {
    func verify(reference: String) async throws -> VerifyResponse {
        guard let encodedRef = reference.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
              let url = URL(string: "(baseURL)/api/pay/verify?reference=(encodedRef)") else {
            throw URLError(.badURL)
        }

        let (data, response) = try await URLSession.shared.data(from: url)

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

        if httpResponse.statusCode == 200 {
            return try JSONDecoder().decode(VerifyResponse.self, from: data)
        } else {
            let errorResponse = try? JSONDecoder().decode(VerifyResponse.self, from: data)
            return errorResponse ?? VerifyResponse(verified: false, amount: nil,
                                                   currency: nil, reference: nil,
                                                   error: "Verification failed")
        }
    }
}

SwiftUI Verification View

// PaymentStatusView.swift (SwiftUI)
import SwiftUI

@MainActor
class PaymentStatusViewModel: ObservableObject {
    @Published var state: PaymentState = .loading

    enum PaymentState {
        case loading
        case success(amount: Double, currency: String, reference: String)
        case failure(message: String)
    }

    func verify(reference: String) {
        Task {
            do {
                let result = try await PaymentService.shared.verify(reference: reference)
                if result.verified, let amount = result.amount, let currency = result.currency {
                    state = .success(amount: amount, currency: currency,
                                    reference: result.reference ?? reference)
                } else {
                    state = .failure(message: result.error ?? "Payment not confirmed")
                }
            } catch {
                state = .failure(message: "Network error: (error.localizedDescription)")
            }
        }
    }
}

struct PaymentStatusView: View {
    let reference: String
    @StateObject private var viewModel = PaymentStatusViewModel()
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Group {
            switch viewModel.state {
            case .loading:
                VStack(spacing: 16) {
                    ProgressView()
                    Text("Verifying payment...")
                        .foregroundColor(.secondary)
                }
            case .success(let amount, let currency, let ref):
                VStack(spacing: 16) {
                    Image(systemName: "checkmark.circle.fill")
                        .font(.system(size: 64))
                        .foregroundColor(.green)
                    Text("Payment Confirmed")
                        .font(.title)
                    Text("(currency) (String(format: "%.2f", amount))")
                        .font(.headline)
                    Text("Ref: (ref)")
                        .font(.caption)
                        .foregroundColor(.secondary)
                    Button("Continue") { dismiss() }
                        .buttonStyle(.borderedProminent)
                }
            case .failure(let message):
                VStack(spacing: 16) {
                    Image(systemName: "xmark.circle.fill")
                        .font(.system(size: 64))
                        .foregroundColor(.red)
                    Text("Verification Failed")
                        .font(.title)
                    Text(message)
                        .foregroundColor(.secondary)
                        .multilineTextAlignment(.center)
                    Button("Retry") { viewModel.verify(reference: reference) }
                        .buttonStyle(.bordered)
                    Button("Contact Support") { /* Navigate to support */ }
                        .foregroundColor(.secondary)
                }
            }
        }
        .padding()
        .onAppear { viewModel.verify(reference: reference) }
    }
}

Learn More

Key Takeaways

  • Verification must go through your backend. The Paystack secret key must not be in your iOS app.
  • Use URLSession with async/await for clean, readable network code in Swift.
  • Decode the JSON response with Codable structs for type safety.
  • Use @MainActor or DispatchQueue.main for UI updates after async verification.
  • Handle network errors, decoding errors, and failed payment states explicitly.
  • Store the reference in UserDefaults before checkout so you can re-verify after an app restart.

Frequently Asked Questions

Should I use @MainActor for payment verification in Swift?
Yes. URLSession async/await runs on a background thread. When you update @Published properties that drive SwiftUI or UIKit, you must be on the main thread. Use @MainActor on your ViewModel class or use await MainActor.run { } inside the Task.
What if verification fails because of a network error on iOS?
Show a retry button. Store the transaction reference in UserDefaults before checkout. If the app is backgrounded or killed and relaunched, check for a pending reference in UserDefaults and run verification again on next launch.
Can I call the Paystack verify API directly from Swift without a backend?
No. The verify endpoint requires your secret key in the Authorization header. Hardcoding the secret key in your Swift app is a critical security vulnerability. It would be visible in the compiled binary. Always route through your backend.
How do I debug Paystack verification in an iOS simulator?
Use Charles Proxy or Network Link Conditioner to inspect HTTP traffic from the simulator. In Xcode, use the Debug Navigator Network tab. Your backend verify endpoint is called from the simulator, so you can check your backend logs to see what Paystack returned.

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