# Java SDK

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

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

The official Java SDK has synchronous methods plus `…Async` twins that return a `CompletableFuture`, typed results
(records) and typed, unchecked exceptions. It needs Java 17 or later, uses the JDK's `java.net.http.HttpClient` and
has one runtime dependency, Jackson databind.

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

Maven:

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

Gradle (Kotlin DSL):

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

The package is `org.email_validation.sdk`.

## Validate an address

```java
import org.email_validation.sdk.EmailValidationClient;
import org.email_validation.sdk.ValidationResult;

try (EmailValidationClient client = EmailValidationClient.builder(System.getenv("EV_API_KEY")).build()) {
  ValidationResult result = client.validate("accepted@sandbox.email-validation.org");
  System.out.println(result.status() + " " + result.reason() + " " + result.action()); // valid accepted send
}
```

Build the client once and share it: it is immutable and thread-safe. Options go in
`ValidateOptions.builder()`: `ipAddress`, `timeoutSeconds` (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 plain strings, so a value added later never breaks decoding: compare them with the constants in
`Status`, `Reason` and `Action`.

## Async

```java
CompletableFuture<ValidationResult> a = client.validateAsync("accepted@sandbox.email-validation.org");
CompletableFuture<ValidationResult> b = client.validateAsync("catch_all@sandbox.email-validation.org");
CompletableFuture.allOf(a, b).join();
System.out.println(a.join().status() + " " + b.join().status());
```

Keep concurrency within your [rate limit](https://email-validation.org/docs/rate-limits/); for long lists use a bulk job. Interrupting the calling
thread, or cancelling the returned future, stops the call; nothing is sent after a cancellation.

## Handle errors

```java
try {
  client.validate(address);
} catch (InsufficientCreditsException e) {
  // buy credits; nothing was charged
} catch (RateLimitException e) {
  System.out.println("retry in " + e.retryAfterSeconds().orElse(1) + " seconds");
} catch (EmailValidationException e) {
  System.err.println(e.code() + " " + e.status() + " " + e.requestId().orElse("-"));
}
```

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

## Retries and timeouts

```java
EmailValidationClient client =
    EmailValidationClient.builder(System.getenv("EV_API_KEY"))
        .timeout(Duration.ofSeconds(30))      // per attempt
        .maxRetries(2)                        // after 429 / 503 answers
        .maxRetryWait(Duration.ofSeconds(10)) // longer Retry-After values are thrown to you instead of waited out
        .build();
```

`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

```java
Credits credits = client.credits(); // balance(), debt(), pending(), lots()
UsageResponse usage =
    client.usage(
        UsageParams.builder()
            .from(LocalDate.of(2026, 10, 19))
            .to(LocalDate.of(2026, 11, 30))
            .groupBy(GroupBy.DAY)
            .build());
ServiceStatus status = client.status();
```

## Bulk jobs

```java
BulkJob job =
    client.bulk().create(BulkInput.emails(List.of("accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org")));

// or from a CSV file (up to 100 MB)
job =
    client.bulk().create(
        BulkInput.file(Path.of("customers.csv")),
        BulkCreateOptions.builder().column("email").hasHeader(true).build());

job =
    client.bulk().waitFor(
        job.id(),
        BulkWaitOptions.builder().onProgress(j -> System.out.println(j.status() + " " + j.processed())).build());

if (BulkJobStatus.COMPLETED.equals(job.status())) {
  byte[] csv =
      client.bulk().results(job.id(), BulkResultsOptions.builder().filter(Status.VALID, Status.RISKY).build());
} else if (BulkJobStatus.READY.equals(job.status())) {
  // it could not start: job.errorCode() is e.g. insufficient_credits (buy credits, then client.bulk().start(id))
}
```

`waitFor` 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. Set `timeout` in `BulkWaitOptions` to give up
earlier. Also: `bulk().get(id)`, `bulk().start(id)` for jobs created with `start(false)` (call it before
`waitFor`), `bulk().cancel(id)`, and `bulk().resultsStream(id)`, which returns an `InputStream` for large files.

## Verify webhooks

```java
// rawBody: the request body bytes exactly as received; headers: for example a Map<String, List<String>>
try {
  WebhookEvent event = Webhooks.verifyWebhook(rawBody, headers, System.getenv("EV_WEBHOOK_SECRET"));
  if (WebhookEventType.BULK_COMPLETED.equals(event.type())) {
    System.out.println("job done " + event.bulkData().jobId());
  }
  // answer 204
} catch (WebhookVerificationException e) {
  // answer 400
}
```

Header names match in any case. `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. `Webhooks.signWebhook` creates a valid signature for local
tests.

## Constants

`Vocabulary` holds `STATUSES`, `REASONS`, `REASONS_BY_STATUS`, `ACTIONS`, `ERROR_CODES`, `WEBHOOK_EVENTS` and
`sandboxAddress(reason)`; `Status`, `Reason`, `Action`, `BulkJobStatus`, `ErrorCode` and `WebhookEventType` hold
string constants. 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

Pass a configured `java.net.http.HttpClient` (a proxy, custom TLS) through
`builder(key).transport(new JavaHttpTransport(httpClient))`; build it with `followRedirects(HttpClient.Redirect.NEVER)`.
Or implement the one-method `Transport` interface (a `TransportRequest` in, a `TransportResponse` out), for example
in tests.
