Email Verification in Swift: Building an iOS Validation Flow with URLSession

Key Takeaways
  • Swift's modern async/await concurrency model makes API integrations clean and readable, with structured error handling through do/try/catch that prevents silent failures.
  • The EmailVerifierAPI v2 endpoint returns JSON that maps directly to Swift Codable structs, enabling type-safe response handling with zero manual parsing.
  • URLSession's native async support (iOS 15+, macOS 12+) eliminates the need for third-party HTTP libraries, keeping your dependency footprint minimal.
  • A reusable EmailVerifier service class can be integrated into SwiftUI signup forms, UIKit registration flows, and server-side Swift applications with identical logic.

Why Verify on the Client Side

Mobile applications collect email addresses in signup forms, account settings, and profile screens. The addresses entered on iOS devices are particularly error-prone because mobile keyboards introduce autocorrect interference, small touch targets lead to mistaps, and users often rush through registration screens. A verification step that validates the email before the form is submitted catches these errors at the point of entry, before the invalid address propagates into your backend systems and email sending pipeline.

This guide walks through integrating the EmailVerifierAPI v2 endpoint into a Swift application using URLSession and async/await. The code runs on iOS 15+, macOS 12+, and server-side Swift.

Testing with cURL

Confirm your API key and review the response format:

curl -X GET "https://www.emailverifierapi.com/v2/verify?email=test@example.com&apikey=YOUR_API_KEY"

Response:

{
  "status": "passed",
  "isDisposable": false,
  "isFreeService": true,
  "isOffensive": false,
  "isRoleAccount": false,
  "isGibberish": false,
  "smtp_check": "success",
  "sub_status": "mailboxExists"
}

Codable Response Model

Swift's Codable protocol maps JSON keys directly to struct properties. Define a struct that mirrors the API response and use CodingKeys for fields that do not follow Swift naming conventions:

import Foundation

struct VerificationResult: Codable {
    let status: String
    let isDisposable: Bool
    let isFreeService: Bool
    let isOffensive: Bool
    let isRoleAccount: Bool
    let isGibberish: Bool
    let smtpCheck: String
    let subStatus: String

    enum CodingKeys: String, CodingKey {
        case status, isDisposable, isFreeService
        case isOffensive, isRoleAccount, isGibberish
        case smtpCheck = "smtp_check"
        case subStatus = "sub_status"
    }

    var isPassed: Bool { status == "passed" }
    var isSafeToSend: Bool {
        isPassed && !isDisposable && !isRoleAccount && !isGibberish
    }
}

The Verification Service

The service class uses URLSession with async/await for a clean, readable API call. Error handling is explicit through Swift's typed error system:

enum VerificationError: Error {
    case invalidURL
    case httpError(statusCode: Int)
    case decodingFailed
}

class EmailVerifier {
    private let apiKey: String
    private let baseURL = "https://www.emailverifierapi.com/v2/verify"
    private let session: URLSession

    init(apiKey: String, session: URLSession = .shared) {
        self.apiKey = apiKey
        self.session = session
    }

    func verify(_ email: String) async throws -> VerificationResult {
        guard let encoded = email.addingPercentEncoding(
            withAllowedCharacters: .urlQueryAllowed
        ),
        let url = URL(string:
            "(baseURL)?email=(encoded)&apikey=(apiKey)"
        ) else {
            throw VerificationError.invalidURL
        }

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

        guard let http = response as? HTTPURLResponse,
              (200...299).contains(http.statusCode) else {
            let code = (response as? HTTPURLResponse)?.statusCode ?? 0
            throw VerificationError.httpError(statusCode: code)
        }

        do {
            return try JSONDecoder().decode(
                VerificationResult.self, from: data
            )
        } catch {
            throw VerificationError.decodingFailed
        }
    }
}

// Usage:
// let verifier = EmailVerifier(apiKey: "YOUR_API_KEY")
// let result = try await verifier.verify("user@example.com")
// print(result.status)       // "passed"
// print(result.subStatus)    // "mailboxExists"
// print(result.isSafeToSend) // true

Integration in a SwiftUI Signup Form

In a SwiftUI view, the verification call can be triggered when the user taps a submit button or when the email field loses focus. The async nature of the API call integrates naturally with SwiftUI's Task modifier:

import SwiftUI

struct SignupView: View {
    @State private var email = ""
    @State private var isVerifying = false
    @State private var verificationMessage = ""
    @State private var isValid = false

    private let verifier = EmailVerifier(
        apiKey: ProcessInfo.processInfo
            .environment["EMAIL_VERIFIER_API_KEY"] ?? ""
    )

    var body: some View {
        VStack(spacing: 16) {
            TextField("Email address", text: $email)
                .textFieldStyle(.roundedBorder)
                .autocapitalization(.none)
                .keyboardType(.emailAddress)

            Button("Verify & Sign Up") {
                Task { await verifyAndSubmit() }
            }
            .disabled(email.isEmpty || isVerifying)

            if !verificationMessage.isEmpty {
                Text(verificationMessage)
                    .foregroundColor(isValid ? .green : .red)
                    .font(.caption)
            }
        }
        .padding()
    }

    private func verifyAndSubmit() async {
        isVerifying = true
        defer { isVerifying = false }
        do {
            let result = try await verifier.verify(email)
            if result.isSafeToSend {
                isValid = true
                verificationMessage = "Email verified!"
            } else if result.isDisposable {
                isValid = false
                verificationMessage = "Please use a permanent email."
            } else {
                isValid = false
                verificationMessage = "Could not verify. Check for typos."
            }
        } catch {
            verificationMessage = "Verification unavailable. Try again."
        }
    }
}

Understanding the Response Fields

The "status" field drives your primary accept/reject logic. "Passed" confirms the mailbox is deliverable. "Failed" means suppress the address. "Unknown" indicates the check was inconclusive, typically a catch-all domain. "Transient" means a temporary error and a retry is appropriate. The convenience computed properties isPassed and isSafeToSend on the Codable struct simplify the most common branching decisions.

The boolean flags provide secondary risk signals that matter in mobile contexts. "isDisposable" is critical for apps that offer free trials or freemium tiers, since disposable addresses are the primary vector for serial trial abuse on mobile. "isGibberish" catches bot-generated registrations. "isFreeService" can inform whether to request additional identity verification for high-value account actions. The "subStatus" field provides the most specific diagnostic detail for debugging integration issues.

Security Considerations

Store your API key securely. Never embed it directly in client-side code that ships in an app bundle. Use server-side environment variables, a backend proxy that relays verification requests, or Apple's Keychain for local secure storage. For production iOS apps, the recommended pattern is to route verification requests through your own backend API, which adds the API key server-side and forwards the result to the client. This prevents key exposure through app decompilation or network interception.

Frequently Asked Questions

Should I verify email on the client (iOS) or server side?

For production apps, route verification through your backend to protect your API key. The Swift code shown here works directly against the API for development and prototyping. In production, replace the direct API call with a call to your own backend endpoint that proxies the verification request and adds the API key server-side.

What iOS version is required for async/await URLSession?

Async/await with URLSession requires iOS 15+ or macOS 12+. For earlier deployment targets, use URLSession's completion handler API with a callback-based wrapper. The Codable response model and verification logic remain identical regardless of concurrency approach.

How do I handle offline scenarios in the iOS app?

If the device is offline or the API is unreachable, the verification call will throw a network error. Catch this error and allow the signup to proceed with a note that the email will be verified when connectivity is restored. Queue the address for server-side verification on next app launch or when the device comes back online.