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)"
}
}
}
Method 1: URLSession with async/await (Recommended)
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.
Related Guides
Add CAPTCHA solving to your Swift apps — get your API key and integrate in minutes.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.