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.SdkThe 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 sendOptions (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.