API Tutorials

Solving CAPTCHAs with Swift and CaptchaAI API

Swift developers building iOS/macOS apps, server-side applications (Vapor), or command-line automation tools encounter CAPTCHAs in form submissions, API interactions, and web scraping. CaptchaAI's HTTP API integrates cleanly with Swift's native URLSession and modern async/await concurrency.

This guide covers reCAPTCHA v2/v3, Cloudflare Turnstile, and image CAPTCHA solving using Foundation's URLSession, plus Alamofire for projects already using it.


Why Swift for CAPTCHA Integration

  • Native async/await — structured concurrency built into the language (Swift 5.5+)
  • URLSession — no external dependencies needed for HTTP calls
  • Codable — automatic JSON encoding/decoding for API responses
  • Cross-platform — works on iOS, macOS, Linux (Swift on Server)
  • Type safety — enums and optionals catch API errors at compile time

Prerequisites

  • Swift 5.5+ (for async/await)
  • Xcode 13+ (for iOS/macOS) or Swift toolchain on Linux
  • CaptchaAI API key (get one here)

For Alamofire (optional):

// Package.swift
dependencies: [
    .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
]

Data Models

import Foundation

struct ApiResponse: Codable {
    let status: Int
    let request: String
}

enum CaptchaType {
    case recaptchaV2(sitekey: String, pageUrl: String)
    case recaptchaV3(sitekey: String, pageUrl: String, action: String, minScore: Double)
    case turnstile(sitekey: String, pageUrl: String)
    case imageBase64(body: String)
}

enum CaptchaError: Error, LocalizedError {
    case apiError(String)
    case timeout
    case invalidResponse
    case networkError(Error)

    var errorDescription: String? {
        switch self {
        case .apiError(let msg): return "API error: \(msg)"
        case .timeout: return "CAPTCHA solve timeout"
        case .invalidResponse: return "Invalid API response"
        case .networkError(let err): return "Network error: \(err.localizedDescription)"
        }
    }
}

Zero external dependencies — uses Foundation only.

class CaptchaSolver {
    private let apiKey: String
    private let baseURL = "https://ocr.captchaai.com"
    private let session: URLSession
    private let pollInterval: TimeInterval = 5.0
    private let maxWait: TimeInterval = 300.0

    init(apiKey: String) {
        self.apiKey = apiKey
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        self.session = URLSession(configuration: config)
    }

    func solve(_ captcha: CaptchaType) async throws -> String {
        let taskId = try await submit(captcha)
        return try await poll(taskId: taskId)
    }

    // MARK: - Submit

    private func submit(_ captcha: CaptchaType) async throws -> String {
        var params: [String: String] = [
            "key": apiKey,
            "json": "1"
        ]

        switch captcha {
        case .recaptchaV2(let sitekey, let pageUrl):
            params["method"] = "userrecaptcha"
            params["googlekey"] = sitekey
            params["pageurl"] = pageUrl

        case .recaptchaV3(let sitekey, let pageUrl, let action, let minScore):
            params["method"] = "userrecaptcha"
            params["googlekey"] = sitekey
            params["pageurl"] = pageUrl
            params["version"] = "v3"
            params["action"] = action
            params["min_score"] = String(minScore)

        case .turnstile(let sitekey, let pageUrl):
            params["method"] = "turnstile"
            params["key"] = sitekey
            params["pageurl"] = pageUrl

        case .imageBase64(let body):
            params["method"] = "base64"
            params["body"] = body
        }

        let url = URL(string: "\(baseURL)/in.php")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = params
            .map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" }
            .joined(separator: "&")
            .data(using: .utf8)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

        let (data, _) = try await session.data(for: request)
        let response = try JSONDecoder().decode(ApiResponse.self, from: data)

        guard response.status == 1 else {
            throw CaptchaError.apiError(response.request)
        }

        return response.request
    }

    // MARK: - Poll

    private func poll(taskId: String) async throws -> String {
        let deadline = Date().addingTimeInterval(maxWait)

        while Date() < deadline {
            try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))

            var components = URLComponents(string: "\(baseURL)/res.php")!
            components.queryItems = [
                URLQueryItem(name: "key", value: apiKey),
                URLQueryItem(name: "action", value: "get"),
                URLQueryItem(name: "id", value: taskId),
                URLQueryItem(name: "json", value: "1")
            ]

            let (data, _) = try await session.data(from: components.url!)
            let response = try JSONDecoder().decode(ApiResponse.self, from: data)

            if response.request == "CAPCHA_NOT_READY" { continue }

            guard response.status == 1 else {
                throw CaptchaError.apiError(response.request)
            }

            return response.request
        }

        throw CaptchaError.timeout
    }

    // MARK: - Balance

    func checkBalance() async throws -> Double {
        var components = URLComponents(string: "\(baseURL)/res.php")!
        components.queryItems = [
            URLQueryItem(name: "key", value: apiKey),
            URLQueryItem(name: "action", value: "getbalance"),
            URLQueryItem(name: "json", value: "1")
        ]

        let (data, _) = try await session.data(from: components.url!)
        let response = try JSONDecoder().decode(ApiResponse.self, from: data)

        guard let balance = Double(response.request) else {
            throw CaptchaError.invalidResponse
        }

        return balance
    }
}

Usage

@main
struct CaptchaApp {
    static func main() async throws {
        let solver = CaptchaSolver(apiKey: "YOUR_API_KEY")

        // Check balance
        let balance = try await solver.checkBalance()
        print("Balance: $\(String(format: "%.2f", balance))")

        // Solve reCAPTCHA v2
        let token = try await solver.solve(
            .recaptchaV2(
                sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
                pageUrl: "https://example.com/login"
            )
        )
        print("Token: \(String(token.prefix(50)))...")

        // Solve Turnstile
        let turnstileToken = try await solver.solve(
            .turnstile(
                sitekey: "0x4AAAAAAAB5...",
                pageUrl: "https://example.com/form"
            )
        )
        print("Turnstile: \(String(turnstileToken.prefix(50)))...")
    }
}

Image CAPTCHA Solving

func solveImageCaptcha(solver: CaptchaSolver, imagePath: String) async throws -> String {
    let imageData = try Data(contentsOf: URL(fileURLWithPath: imagePath))
    let base64 = imageData.base64EncodedString()
    return try await solver.solve(.imageBase64(body: base64))
}

// From UIImage (iOS)
func solveFromUIImage(solver: CaptchaSolver, image: UIImage) async throws -> String {
    guard let data = image.pngData() else {
        throw CaptchaError.invalidResponse
    }
    let base64 = data.base64EncodedString()
    return try await solver.solve(.imageBase64(body: base64))
}

Concurrent Solving with TaskGroup

func solveBatch(
    solver: CaptchaSolver,
    tasks: [CaptchaType]
) async -> [Result<String, Error>] {
    await withTaskGroup(of: (Int, Result<String, Error>).self) { group in
        for (index, task) in tasks.enumerated() {
            group.addTask {
                do {
                    let token = try await solver.solve(task)
                    return (index, .success(token))
                } catch {
                    return (index, .failure(error))
                }
            }
        }

        var results = Array<Result<String, Error>>(repeating: .failure(CaptchaError.timeout), count: tasks.count)
        for await (index, result) in group {
            results[index] = result
        }
        return results
    }
}

// Usage
let tasks: [CaptchaType] = [
    .recaptchaV2(sitekey: "KEY_A", pageUrl: "https://site-a.com"),
    .turnstile(sitekey: "KEY_B", pageUrl: "https://site-b.com"),
    .recaptchaV2(sitekey: "KEY_C", pageUrl: "https://site-c.com"),
]

let results = await solveBatch(solver: solver, tasks: tasks)
for (i, result) in results.enumerated() {
    switch result {
    case .success(let token):
        print("Task \(i): \(String(token.prefix(50)))...")
    case .failure(let error):
        print("Task \(i) failed: \(error)")
    }
}

Error Handling with Retry

func solveWithRetry(
    solver: CaptchaSolver,
    captcha: CaptchaType,
    maxRetries: Int = 3
) async throws -> String {
    let retryableErrors = ["ERROR_NO_SLOT_AVAILABLE", "ERROR_CAPTCHA_UNSOLVABLE"]

    for attempt in 0...maxRetries {
        if attempt > 0 {
            let delay = pow(2.0, Double(attempt)) + Double.random(in: 0...2)
            print("Retry \(attempt)/\(maxRetries) after \(delay)s")
            try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
        }

        do {
            return try await solver.solve(captcha)
        } catch CaptchaError.apiError(let msg) where retryableErrors.contains(where: { msg.contains($0) }) {
            if attempt == maxRetries { throw CaptchaError.apiError(msg) }
            continue
        } catch CaptchaError.timeout where attempt < maxRetries {
            continue
        }
    }

    throw CaptchaError.timeout
}

iOS SwiftUI Integration

import SwiftUI

@MainActor
class CaptchaViewModel: ObservableObject {
    @Published var token: String?
    @Published var error: String?
    @Published var isLoading = false

    private let solver = CaptchaSolver(apiKey: "YOUR_API_KEY")

    func solveCaptcha(sitekey: String, pageUrl: String) {
        isLoading = true
        token = nil
        error = nil

        Task {
            do {
                let result = try await solver.solve(
                    .recaptchaV2(sitekey: sitekey, pageUrl: pageUrl)
                )
                token = result
            } catch {
                self.error = error.localizedDescription
            }
            isLoading = false
        }
    }
}

struct CaptchaView: View {
    @StateObject private var viewModel = CaptchaViewModel()

    var body: some View {
        VStack(spacing: 16) {
            Button("Solve CAPTCHA") {
                viewModel.solveCaptcha(
                    sitekey: "SITEKEY",
                    pageUrl: "https://example.com"
                )
            }
            .disabled(viewModel.isLoading)

            if viewModel.isLoading {
                ProgressView("Solving...")
            }

            if let token = viewModel.token {
                Text("Solved!")
                    .foregroundColor(.green)
                Text(String(token.prefix(50)) + "...")
                    .font(.caption)
            }

            if let error = viewModel.error {
                Text(error)
                    .foregroundColor(.red)
            }
        }
        .padding()
    }
}

Vapor (Server-Side Swift) Integration

import Vapor

struct SolveRequest: Content {
    let sitekey: String
    let pageUrl: String
    let type: String?
}

struct SolveResponse: Content {
    let token: String
}

func routes(_ app: Application) throws {
    let solver = CaptchaSolver(apiKey: Environment.get("CAPTCHAAI_KEY") ?? "")

    app.post("api", "captcha", "solve") { req async throws -> SolveResponse in
        let body = try req.content.decode(SolveRequest.self)

        let captchaType: CaptchaType
        switch body.type {
        case "turnstile":
            captchaType = .turnstile(sitekey: body.sitekey, pageUrl: body.pageUrl)
        default:
            captchaType = .recaptchaV2(sitekey: body.sitekey, pageUrl: body.pageUrl)
        }

        let token = try await solver.solve(captchaType)
        return SolveResponse(token: token)
    }
}

Submitting Solved Tokens

func submitFormWithToken(
    url: String,
    token: String,
    formData: [String: String]
) async throws -> (Data, URLResponse) {
    var params = formData
    params["g-recaptcha-response"] = token

    var request = URLRequest(url: URL(string: url)!)
    request.httpMethod = "POST"
    request.httpBody = params
        .map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" }
        .joined(separator: "&")
        .data(using: .utf8)
    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

    return try await URLSession.shared.data(for: request)
}

Troubleshooting

Error Cause Fix
ERROR_WRONG_USER_KEY Invalid API key Verify key at dashboard
ERROR_ZERO_BALANCE No funds Top up account
NSURLErrorDomain Network issue Check connectivity, App Transport Security
DecodingError Unexpected JSON Enable ignoreUnknownKeys or check response
CancellationError Task cancelled Ensure Task isn't cancelled before completion
ATS blocked HTTP iOS blocks non-HTTPS CaptchaAI uses HTTPS — no ATS issue

FAQ

Does CaptchaAI have a Swift package?

CaptchaAI provides a REST API that works with URLSession (built into Swift). No external package needed.

Can I use this on iOS?

Yes. Use the async/await solver from a SwiftUI ViewModel or UIKit ViewController. Network calls run on background threads automatically.

Which Swift version do I need?

Swift 5.5+ for async/await. For older Swift, use completion handler-based URLSession calls.

Does this work with server-side Swift (Vapor)?

Yes. The solver uses Foundation's URLSession which works on Linux. For Vapor projects, you can also use AsyncHTTPClient.



Add CAPTCHA solving to your Swift apps — get your API key and integrate in minutes.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.