Rust SDK

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, which call the API directly.

Install

[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

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). 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

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; 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

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

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

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

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

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 and 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.

Updated · Maintained by the email-validation.org team