# Rust SDK

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

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

The official Rust SDK is async (`async fn` on Tokio) with typed results, typed errors and an optional blocking
client. Its HTTP stack is `reqwest` with rustls, and redirects are never followed.

**Not published yet.** The crate `email-validation-sdk` is not on crates.io 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

```toml
[dependencies]
email-validation-sdk = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

The library is imported as `email_validation`. Features: `default-transport` (on by default: the built-in `reqwest`
transport) and `blocking` (adds `email_validation::blocking::Client`).

## Validate an address

```rust
use email_validation::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("EV_API_KEY")?)?;
    let result = client.validate("accepted@sandbox.email-validation.org").await?;
    println!("{} {} {}", result.status, result.reason, result.action); // valid accepted send
    Ok(())
}
```

Build the client once and share it: it is cheap to clone and safe to use from many tasks. `validate_with` takes a
`ValidateOptions`: `ip_address`, `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 enums you can `match`
on; a value this SDK version does not know yet decodes into `Other(raw)` instead of failing.

## Async and cancellation

```rust
let (a, b) = tokio::join!(
    client.validate("accepted@sandbox.email-validation.org"),
    client.validate("catch_all@sandbox.email-validation.org"),
);
println!("{} {}", a?.status, b?.status);
```

Keep concurrency within your [rate limit](https://email-validation.org/docs/rate-limits/); for long lists use a bulk job. Cancel a call by
dropping its future, for example with `tokio::time::timeout`: nothing more is sent. Without an async runtime, enable
the `blocking` feature and use `email_validation::blocking::Client`, which has the same methods without `.await`.

## Handle errors

```rust
use email_validation::ErrorKind;

match client.validate(address).await {
    Ok(result) => println!("{}", result.status),
    Err(err) => match err.kind() {
        ErrorKind::InsufficientCredits => println!("buy credits; nothing was charged"),
        ErrorKind::RateLimit => println!("retry in {:?} s", err.retry_after()),
        _ => println!("{} {} {:?}", err.code(), err.status(), err.request_id()),
    },
}
```

Every error is an `email_validation::Error` with `kind()`, `status()` (0 when no answer arrived), `code()`,
`request_id()` and `retry_after()`. Kinds: `InvalidRequest`, `Authentication`, `InsufficientCredits`,
`PermissionDenied`, `NotFound`, `Conflict`, `RateLimit`, `ServiceUnavailable`, `Server`, `Connection` and
`EmailValidation` (an answer that is not JSON).

## Retries and timeouts

```rust
use std::time::Duration;

let client = Client::builder(std::env::var("EV_API_KEY")?)
    .timeout(Duration::from_secs(30))        // per attempt
    .max_retries(2)                          // after 429 / 503 answers
    .max_retry_wait(Duration::from_secs(10)) // longer Retry-After values are returned to you
    .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

```rust
use email_validation::{GroupBy, UsageParams};

let credits = client.credits().await?; // balance, debt, pending, lots
let usage = client
    .usage(UsageParams::new().from("2026-10-19").to("2026-11-30").group_by(GroupBy::Day))
    .await?;
let status = client.status().await?;
```

## Bulk jobs

```rust
use email_validation::{BulkCreateOptions, BulkInput, BulkJobStatus, ResultsOptions, Status, WaitOptions};

let job = client
    .bulk()
    .create(
        BulkInput::emails(["accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"]),
        BulkCreateOptions::new(),
    )
    .await?;

// or from a CSV file (up to 100 MB)
let csv = std::fs::read("customers.csv")?;
let job = client
    .bulk()
    .create(BulkInput::csv(csv), BulkCreateOptions::new().column("email").has_header(true))
    .await?;

let job = client
    .bulk()
    .wait(&job.id, WaitOptions::new().on_progress(|j| println!("{} {}", j.status, j.processed)))
    .await?;

if job.status == BulkJobStatus::Completed {
    let csv = client
        .bulk()
        .results(&job.id, ResultsOptions::new().filter(vec![Status::Valid, Status::Risky]))
        .await?;
} else if job.status == BulkJobStatus::Ready {
    // it could not start: job.error_code 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
`error_code`: it could not start and does not move on by itself. Set `.timeout(…)` in `WaitOptions` to give up
earlier. Also: `bulk().get(id)`, `bulk().start(id)` for jobs created with `.start(false)` (call it before `wait`),
`bulk().cancel(id)`, and `bulk().results_stream(id, …)`, which streams large files chunk by chunk.

## Verify webhooks

```rust
use email_validation::{verify_webhook, WebhookHeaders, WebhookPayload};

/// `raw_body` is the request body exactly as received; `headers` can be an `http::HeaderMap` (axum, hyper),
/// a `HashMap` or a list of `(name, value)` pairs.
fn on_delivery(raw_body: &[u8], headers: &impl WebhookHeaders, secret: &str) -> u16 {
    let Ok(event) = verify_webhook(raw_body, headers, secret) else {
        return 400;
    };
    if let Ok(WebhookPayload::BulkCompleted(data)) = event.payload() {
        println!("job done {}", data.job_id);
    }
    204
}
```

The 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. `sign_webhook` creates a valid signature for local tests.

## Constants

`STATUSES`, `REASONS`, `REASONS_BY_STATUS`, `REASON_INFO`, `ACTIONS`, `ERROR_CODES`, `WEBHOOK_EVENTS`, the enums
`Status`, `Reason` and `Action` (`Reason::status()` and `Reason::action()` give a reason's fixed status and action)
and `sandbox_address(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

`ReqwestTransport::from_client` wraps your own `reqwest::Client` (proxies, custom TLS; build it with
`redirect(reqwest::redirect::Policy::none())`) and goes in with `Client::builder(key).transport(…)`. Or implement
the `Transport` trait (an `HttpRequest` in, an `HttpResponse` or a `TransportError` out), for example in tests;
without the `default-transport` feature the SDK does not depend on `reqwest` at all.
