McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Verify Paystack Payments in iOS with Swift
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

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.

In this article

  1. 1. Payment Service: Verify
  2. 2. SwiftUI Verification View
  3. 3. Learn More

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) }
    }
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Learn More

This guide is part of the Paystack integration guides by language and framework series.

Sign up for the McTaba newsletter

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.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Accept Payments with Paystack in iOS with Swift

Accept Paystack payments in a native iOS app with Swift. This guide covers backend-driven transaction initialization, loading the Paystack checkout in WKWebView, detecting the callback redirect, and integrating with SwiftUI.

Read more

Handle Paystack Webhooks in iOS with Swift

Paystack webhooks go to your server. This guide explains building the backend webhook handler and delivering payment events to an iOS Swift app via Apple Push Notification service (APNs) or Firebase Cloud Messaging.

Read more

Transaction Not Found on Paystack Verify

Paystack returns "Transaction not found" when the reference you pass to the verify endpoint does not match any transaction in the current environment. The causes range from wrong reference strings to test/live key confusion to URL encoding problems. This guide covers every cause with specific fixes.

Read more

Idempotency in Paystack Payment Flows

Duplicate charges are the nightmare scenario for any payment integration. A customer clicks pay twice, a network timeout triggers a retry, or a webhook fires before the redirect callback. Idempotency is the engineering discipline that makes sure the same payment operation produces the same result no matter how many times it runs.

Read more

Paystack Mixed Content and HTTPS Errors

Your site is served over HTTP but Paystack scripts load over HTTPS. Modern browsers block this mix. The fix is to serve your entire site over HTTPS.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.