Swift SDK
The official Swift SDK uses async/await, is Sendable and builds in the Swift 6 language mode with strict concurrency checking. It needs the Swift 6.0 tools or later and supports macOS 12, iOS 15, tvOS 15 and watchOS 8 or later. Its one dependency is swift-crypto, for webhook signatures.
Not published yet. The Swift package email-validation-swift is not on the Swift Package Index yet, so the install step below does not work today. The SDK is complete and tested; it will be published once the registry account and the licence are in place. Until then, use the code samples, which call the API directly.
Install
With Swift Package Manager, in Package.swift (the repository URL is listed here once the package is published):
dependencies: [
.package(url: "<repository URL>", from: "1.0.0")
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "EmailValidation", package: "email-validation-swift")])
]In Xcode: File > Add Package Dependencies…, then add the EmailValidation library to your target.
Validate an address
import EmailValidation
import Foundation
let client = try EmailValidationClient(apiKey: ProcessInfo.processInfo.environment["EV_API_KEY"] ?? "")
let result = try await client.validate("accepted@sandbox.email-validation.org")
print(result.status, result.reason, result.action) // valid accepted sendKeep the API key on your server: do not ship it inside an app. Construct the client once and share it; it is safe to use from many tasks. Parameters: ipAddress:, timeout: (seconds the check may take, 5–15) and fresh:. fresh: true checks the address again even when it was checked in the last 12 months (see repeat checks). Status, Reason and Action have named constants (.valid, .catchAll, .send); a value added to the API later still decodes, so a switch over one needs a default: branch.
Async and cancellation
async let a = client.validate("accepted@sandbox.email-validation.org")
async let b = client.validate("catch_all@sandbox.email-validation.org")
for r in try await [a, b] { print(r.email, r.status) }Keep concurrency within your rate limit; for long lists use a bulk job. Cancelling the calling Task stops a request, a retry wait or a bulk.wait poll at once (error kind .connection, code .aborted), and nothing more is sent.
Handle errors
do {
_ = try await client.validate(address)
} catch let error as EmailValidationError {
switch error.kind {
case .insufficientCredits: print("buy credits; nothing was charged")
case .rateLimit: print("retry in \(error.retryAfter ?? 1) seconds")
default: print(error.code, error.status, error.requestId ?? "-")
}
}Every API failure is an EmailValidationError with kind, status (0 when no answer arrived), code, requestId and retryAfter. Kinds: .invalidRequest, .authentication, .insufficientCredits, .permissionDenied, .notFound, .conflict, .rateLimit, .serviceUnavailable, .server, .connection and .general (an answer that is not JSON). Invalid client options throw InvalidArgumentError.
Retries and timeouts
let client = try EmailValidationClient(
apiKey: key,
timeout: 30, // seconds per attempt
maxRetries: 2, // after 429 / 503 answers
maxRetryWait: 10 // longer Retry-After values are thrown to you instead of waited out
)429 and 503 answers are retried with backoff, honouring Retry-After. A daily key cap is never retried, and a validation is never retried after a network failure, because it may already have been charged.
Credits, usage and status
let credits = try await client.credits() // balance, debt, pending, lots
let usage = try await client.usage(from: "2026-10-19", to: "2026-11-30", groupBy: .day)
let status = try await client.status()Bulk jobs
var job = try await client.bulk.create(emails: ["accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"])
// or from a CSV file (up to 100 MB)
job = try await client.bulk.create(fileAt: URL(fileURLWithPath: "customers.csv"), column: "email", hasHeader: true)
job = try await client.bulk.wait(job.id) { job in print(job.status, job.processed) }
if job.status == .completed {
let csv = try await client.bulk.results(job.id, filter: [.valid, .risky])
} else if job.status == .ready {
// it could not start: job.errorCode is e.g. insufficient_credits (buy credits, then client.bulk.start(job.id))
}wait returns when the job is completed, failed, cancelled or expired, or when it is still ready with an errorCode: it could not start and does not move on by itself. Pass timeout: (seconds) to give up earlier. Also: bulk.get, bulk.start for jobs created with start: false (call it before wait), bulk.cancel, and bulk.resultsStream, an AsyncSequence of Data chunks for large files.
Verify webhooks
// rawBody: the request body exactly as received (Data or String); headers: [String: String] or [String: [String]]
do {
let event = try verifyWebhook(payload: rawBody, headers: headers, secret: webhookSecret)
if case .bulk(let data) = event.data, event.type == .bulkCompleted {
print("job done", data.jobId)
}
// answer 204
} catch is WebhookVerificationError {
// answer 400
}event.type can also be webhook.test, sent when you press Send test in the dashboard: answer it with a 2xx like any other event. signWebhook(msgId:timestamp:payload:secret:) creates a valid signature for local tests.
Constants
Vocabulary holds statuses, reasons, reasonsByStatus, reasonInfo, actions, errorCodes, webhookEvents and sandboxAddress(_:); each vocabulary type has named constants and known. See statuses and reasons and test addresses.
Bring your own HTTP client
The default URLSessionTransport refuses redirects, stores no cookies and caches nothing; URLSessionTransport(configuration:) sets a proxy or other connection options. To replace it, for example in tests, implement the HTTPTransport protocol (send(_:) async throws -> HTTPResponse) and pass it as transport:.