# C# / .NET SDK

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

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

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](https://email-validation.org/docs/code-samples/), which
call the API directly.

## Install

```bash
dotnet add package EmailValidation.Sdk
```

The namespace is `EmailValidation`.

## Validate an address

```csharp
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](https://email-validation.org/docs/api/validate/#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

```csharp
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](https://email-validation.org/docs/rate-limits/); for long lists use a
bulk job.

## Handle errors

```csharp
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

```csharp
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

```csharp
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

```csharp
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

```csharp
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.
