# Swift SDK

URL: https://email-validation.org/docs/sdks/swift/
Updated: 2026-10-23

> Documentation index: https://email-validation.org/llms.txt

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](https://github.com/apple/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](https://email-validation.org/docs/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):

```swift
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

```swift
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 send
```

Keep 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](https://email-validation.org/docs/api/validate/#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

```swift
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](https://email-validation.org/docs/rate-limits/); 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

```swift
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

```swift
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

```swift
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

```swift
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

```swift
// 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](https://email-validation.org/docs/statuses/) and [test addresses](https://email-validation.org/docs/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:`.
