C# / .NET SDK

The official .NET SDK is an async client (Task<T> with a CancellationToken on every method) with typed results and typed errors. It targets .NET 8 and later, uses only the .NET base library (HttpClient, System.Text.Json) and is safe for trimming and native AOT.

Not published yet. The NuGet package EmailValidation.Sdk is not on NuGet 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

dotnet add package EmailValidation.Sdk

The namespace is EmailValidation.

Validate an address

using EmailValidation;

using var client = new EmailValidationClient(Environment.GetEnvironmentVariable("EV_API_KEY")!);

var result = await client.ValidateAsync("accepted@sandbox.email-validation.org");
Console.WriteLine($"{result.Status} {result.Reason} {result.Action}"); // valid accepted send

Options (ValidateOptions): 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). Enumerated values are strings: compare them with the constants, for example result.Action == EmailAction.Remove. Construct the client once and share it; it is thread-safe.

Async

var results = await Task.WhenAll(
    client.ValidateAsync("accepted@sandbox.email-validation.org"),
    client.ValidateAsync("catch_all@sandbox.email-validation.org"));
foreach (var r in results) Console.WriteLine($"{r.Email} {r.Status}");

Every method takes a CancellationToken; cancelling it stops the call at once with the standard OperationCanceledException. Keep concurrency within your rate limit; for long lists use a bulk job.

Handle errors

try
{
    await client.ValidateAsync(address);
}
catch (InsufficientCreditsException)
{
    // buy credits; nothing was charged
}
catch (RateLimitException e)
{
    Console.WriteLine($"retry in {e.RetryAfter} seconds");
}
catch (EmailValidationException e)
{
    Console.WriteLine($"{e.Code} {e.Status} {e.RequestId}");
}

Every error has Status (0 when no answer arrived), Code, RequestId and RetryAfter. Classes: InvalidRequestException, AuthenticationException, InsufficientCreditsException, PermissionDeniedException, NotFoundException, ConflictException, RateLimitException, ServiceUnavailableException, ServerException and ConnectionException, all deriving from EmailValidationException.

Retries and timeouts

using var client = new EmailValidationClient(apiKey, new EmailValidationClientOptions
{
    Timeout = TimeSpan.FromSeconds(30),      // per attempt
    MaxRetries = 2,                          // after 429 / 503 answers
    MaxRetryWait = TimeSpan.FromSeconds(10), // longer Retry-After values are raised 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. Keep the client timeout a few seconds above the TimeoutSeconds you send.

Credits, usage and status

var credits = await client.CreditsAsync(); // Balance, Debt, Pending, Lots
var usage = await client.UsageAsync(new UsageOptions
{
    From = new DateOnly(2026, 10, 19),
    To = new DateOnly(2026, 11, 30),
    GroupBy = UsageGroupBy.Day,
});
var status = await client.StatusAsync();

usage.Days holds the items when grouped by day, usage.Keys when grouped by key.

Bulk jobs

var job = await client.Bulk.CreateAsync(["accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"]);

var file = await BulkFile.FromPathAsync("customers.csv");
job = await client.Bulk.CreateAsync(file, new BulkCreateOptions { Column = "email", HasHeader = true });

job = await client.Bulk.WaitAsync(job.Id, new BulkWaitOptions
{
    OnProgress = j => Console.WriteLine($"{j.Status} {j.Processed}"),
});
if (job.Status == BulkJobStatus.Completed)
{
    var csvBytes = await client.Bulk.ResultsAsync(job.Id,
        new BulkResultsOptions { Filter = new[] { Status.Valid, Status.Risky } });
}
else if (job.Status == BulkJobStatus.Ready)
{
    Console.WriteLine($"could not start: {job.ErrorCode}"); // e.g. insufficient_credits: buy credits, then StartAsync
}

WaitAsync returns when the job is completed, failed, cancelled or expired, or when it is still ready with an ErrorCode: it could not start, for example insufficient_credits, and does not move on by itself. Set Timeout to give up earlier.

Also: client.Bulk.GetAsync(jobId), client.Bulk.StartAsync(jobId) for jobs created with Start = false (call it before WaitAsync), and client.Bulk.CancelAsync(jobId). ResultsAsync returns the whole file as bytes; ResultsStreamAsync returns a Stream for large files. To download the full file (Filter = "all", original columns) in parts, set Range = "bytes=…"; other shapes cannot be fetched in parts and throw InvalidRequestException when given a range.

Verify webhooks

app.MapPost("/webhooks/email-validation", async (HttpRequest request) =>
{
    using var body = new MemoryStream();
    await request.Body.CopyToAsync(body);
    try
    {
        var evt = Webhooks.Verify(body.ToArray(), name => string.Join(' ', request.Headers[name].ToArray()),
            Environment.GetEnvironmentVariable("EV_WEBHOOK_SECRET")!);
        if (evt.Type == WebhookEventType.BulkCompleted)
            Console.WriteLine($"job done {evt.GetBulkData().JobId}");
        return Results.NoContent();
    }
    catch (WebhookVerificationException)
    {
        return Results.BadRequest();
    }
});

evt.Type can also be webhook.test, sent when you press Send test in the dashboard: answer it with a 2xx like any other event. Deduplicate on the webhook-id header (retries reuse it). Webhooks.Sign creates a valid signature for local tests.

Constants

EmailValidationConstants lists the API's vocabularies: Statuses, Reasons, ReasonsByStatus, ReasonInfo, Actions, SmtpProviders, BulkJobStatuses, TerminalBulkJobStatuses, ErrorCodes, WebhookEvents, DefaultBaseUrl, SandboxDomain and DefaultWebhookTolerance, with IsStatus, IsReason and SandboxAddress(reason). Single values are constants: Status.Valid, Reason.CatchAll, EmailAction.Remove, BulkJobStatus.Completed, ErrorCode.RateLimited, WebhookEventType.BulkCompleted, …

Bring your own HTTP client

Set HttpClient in the options to use your own HttpClient (proxies, custom TLS, IHttpClientFactory) with AllowAutoRedirect = false on its handler, or Transport to any IEmailValidationTransport (one SendAsync method), for example in tests. The SDK never disposes a client or transport you pass in.

Updated · Maintained by the email-validation.org team