# Kotlin SDK

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

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

The official Kotlin SDK is built on coroutines: every operation is a `suspend` function, with typed models and typed
exceptions. It runs on JVM 11 or later and avoids `java.net.http` and `java.time`, so it suits Android too (its tests
run on the JVM). It needs a Kotlin 2.2+ compiler and depends only on `kotlinx-coroutines-core` and
`kotlinx-serialization-json`.

**Not published yet.** The Maven artifact `org.email-validation:email-validation-sdk-kotlin` is not on Maven Central 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

Gradle (Kotlin DSL):

```kotlin
dependencies {
    implementation("org.email-validation:email-validation-sdk-kotlin:1.0.0")
}
```

Maven:

```xml
<dependency>
  <groupId>org.email-validation</groupId>
  <artifactId>email-validation-sdk-kotlin</artifactId>
  <version>1.0.0</version>
</dependency>
```

The package is `org.email_validation.kotlin`.

## Validate an address

```kotlin
import org.email_validation.kotlin.EmailValidationClient

suspend fun main() {
    EmailValidationClient(System.getenv("EV_API_KEY")).use { client ->
        val result = client.validate("accepted@sandbox.email-validation.org")
        println("${result.status} ${result.reason} ${result.action}") // valid accepted send
    }
}
```

Construct the client once and share it: it is safe to use from many coroutines and threads. Named 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` are value classes with named
constants (`Status.VALID`, `Action.REMOVE`); a value added to the API later is kept as its raw string, so keep an
`else` branch in `when`.

## Async and cancellation

```kotlin
coroutineScope {
    val a = async { client.validate("accepted@sandbox.email-validation.org") }
    val b = async { client.validate("catch_all@sandbox.email-validation.org") }
    listOf(a.await(), b.await()).forEach { println("${it.email} ${it.status}") }
}
```

Keep concurrency within your [rate limit](https://email-validation.org/docs/rate-limits/); for long lists use a bulk job. Cancelling the calling
coroutine, or `withTimeout` around a call, stops it at once with the standard `CancellationException`, also during a
retry wait, and it is never retried.

## Handle errors

```kotlin
try {
    client.validate(address)
} catch (e: InsufficientCreditsException) {
    // buy credits; nothing was charged
} catch (e: RateLimitException) {
    println("retry in ${e.retryAfter ?: 1} seconds")
} catch (e: EmailValidationException) {
    println("${e.code} ${e.status} ${e.requestId}")
}
```

Every error extends the unchecked `EmailValidationException` with `status` (0 when no answer arrived), `code`,
`requestId` and `retryAfter`. Subclasses: `InvalidRequestException`, `AuthenticationException`,
`InsufficientCreditsException`, `PermissionDeniedException`, `NotFoundException`, `ConflictException`,
`RateLimitException`, `ServiceUnavailableException`, `ServerException` and `ConnectionException`.

## Retries and timeouts

```kotlin
val client = EmailValidationClient(System.getenv("EV_API_KEY")) {
    timeout = 30.seconds      // per attempt
    maxRetries = 2            // after 429 / 503 answers
    maxRetryWait = 10.seconds // 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

```kotlin
val credits = client.credits() // balance, debt, pending, lots
val usage = client.usage(from = "2026-10-19", to = "2026-11-30", groupBy = UsageGroupBy.DAY)
val status = client.status()
```

## Bulk jobs

```kotlin
var job = client.bulk.create(listOf("accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"))

// or from a CSV file (up to 100 MB)
job = client.bulk.create(File("customers.csv").readBytes(), BulkContentType.CSV, columnName = "email", hasHeader = true)

job = client.bulk.wait(job.id) { println("${it.status} ${it.processed}") }

if (job.status == BulkJobStatus.COMPLETED) {
    val csv: String = client.bulk.resultsText(job.id, filter = listOf("valid", "risky"))
} else if (job.status == BulkJobStatus.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 =` to give up earlier. Also:
`bulk.get`, `bulk.start` for jobs created with `start = false` (call it before `wait`), `bulk.cancel`, `bulk.results`
(the bytes) and `bulk.resultsStream`, an `InputStream` for large files.

## Verify webhooks

```kotlin
fun handle(rawBody: ByteArray, headers: Map<String, String>): Int {
    val event = try {
        verifyWebhook(rawBody, headers, System.getenv("EV_WEBHOOK_SECRET"))
    } catch (e: WebhookVerificationException) {
        return 400
    }
    if (event.type == WebhookEventType.BULK_COMPLETED) println("job done ${event.bulkData().jobId}")
    return 204
}
```

Header names match in any case, and `Map<String, List<String>>` header maps work too. `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` creates a valid signature for local tests.

## Constants

`STATUSES`, `REASONS`, `REASONS_BY_STATUS`, `REASON_INFO`, `ACTIONS`, `ERROR_CODES`, `WEBHOOK_EVENTS`, the value
classes `Status`, `Reason` and `Action`, and `sandboxAddress(reason)` mirror the API's vocabulary. 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 `UrlConnectionTransport` uses `HttpURLConnection` on `Dispatchers.IO` and never follows redirects. To use
OkHttp, Ktor or a fake in your tests, implement the `Transport` interface (`suspend fun send(request): HttpResponse`)
and set it in the builder: `EmailValidationClient(key) { transport = MyTransport() }`.
