Java SDK
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, which call the API directly.
Install
Maven:
<dependency>
<groupId>org.email-validation</groupId>
<artifactId>email-validation-sdk</artifactId>
<version>1.0.0</version>
</dependency>Gradle (Kotlin DSL):
implementation("org.email-validation:email-validation-sdk:1.0.0")The package is org.email_validation.sdk.
Validate an address
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). 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
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; 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
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
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
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
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
// 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 and 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.