The idiomatic way to call an external API from ASP.NET is a typed HttpClient: a small service class that receives a pre-configured HttpClient from IHttpClientFactory, sends the request, and deserializes the JSON into a C# record. You register it once in Program.cs, inject it wherever you need it, and it handles email verification without any endpoint or controller ever seeing the raw key. The rule that outranks everything else here: keep the API key server-side, in configuration or a secrets store — never in client-side Blazor WASM, a JavaScript bundle, or anything that ships to a browser.
Why verify on the server
Verification requires your secret vae_... key, and a secret is only secret while it stays on infrastructure you control. In a .NET solution that means your ASP.NET server, an API project, or a background worker — not a Blazor WebAssembly client, not a MAUI app, and never a string compiled into anything a user can download and decompile. A leaked key can be drained of credits fast. Read the key from configuration (appsettings.json for local dev, environment variables or a vault in production) and inject it. The broader guidance is in how to protect your API keys.
The response DTO
Model only the fields you act on. The API returns { "ok": true, "result": { ... } }, so mirror that shape with two records and let System.Text.Json map the camelCase JSON onto them.
// VerificationModels.cs
using System.Text.Json.Serialization;
public record VerifyResponse(
[property: JsonPropertyName("ok")] bool Ok,
[property: JsonPropertyName("result")] VerificationResult Result);
public record VerificationResult(
[property: JsonPropertyName("status")] string Status, // deliverable | undeliverable | risky | unknown
[property: JsonPropertyName("subStatus")] string? SubStatus,
[property: JsonPropertyName("score")] int Score,
[property: JsonPropertyName("suggestion")] string? Suggestion);The typed HttpClient service
This service does one job: POST an address to /v1/verify and return the typed result. The base address, timeout and Authorization header are configured once at registration, so the method body stays small.
// EmailVerificationClient.cs
using System.Net.Http.Json;
public class EmailVerificationClient
{
private readonly HttpClient _http;
public EmailVerificationClient(HttpClient http) => _http = http;
public async Task<VerificationResult?> VerifyAsync(string email, CancellationToken ct = default)
{
try
{
var res = await _http.PostAsJsonAsync("/v1/verify", new { email }, ct);
if (!res.IsSuccessStatusCode)
{
// 4xx/5xx (including 429 rate-limit, 402 out-of-credits):
// return null and let the caller decide (fail open).
return null;
}
var body = await res.Content.ReadFromJsonAsync<VerifyResponse>(cancellationToken: ct);
return body?.Result;
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
// Network error or timeout → null, so the caller can fail open.
return null;
}
}
}Register it in Program.cs
Bind the key from configuration and attach it as a default header. Setting a Timeout here means every call is capped without repeating yourself — a live SMTP probe can occasionally be slow, and you never want it to stall a request thread.
// Program.cs
builder.Services.AddHttpClient<EmailVerificationClient>(client =>
{
client.BaseAddress = new Uri("https://api.verifyany.email");
client.Timeout = TimeSpan.FromSeconds(10);
// Key comes from configuration / environment — never hard-coded.
var apiKey = builder.Configuration["VerifyAnyEmail:ApiKey"];
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Add("X-VAE-Source", "dotnet-app");
});Set the value out of source control. Locally use user-secrets or appsettings.Development.json; in production use an environment variable such as VerifyAnyEmail__ApiKey or a managed secret store.
Branch on the status in a controller
The API returns one of four values in result.Status. Map each to a clear action.
// SignupController.cs
[ApiController]
[Route("api/[controller]")]
public class SignupController : ControllerBase
{
private readonly EmailVerificationClient _verifier;
public SignupController(EmailVerificationClient verifier) => _verifier = verifier;
[HttpPost]
public async Task<IActionResult> Post([FromBody] SignupRequest req, CancellationToken ct)
{
var result = await _verifier.VerifyAsync(req.Email, ct);
// Fail open: if verification could not run (null), don't block a real user.
if (result is null)
return await Register(req);
switch (result.Status)
{
case "undeliverable":
// Mailbox doesn't exist → block. Sending guarantees a hard bounce.
return BadRequest(new { error = "undeliverable_email", suggestion = result.Suggestion });
case "risky":
// Reachable but lower confidence (catch-all / role / disposable).
// Allow, but flag for review if you want tighter control.
return await Register(req, flagForReview: true);
case "deliverable":
case "unknown": // unknown = inconclusive probe → fail open, allow.
default:
return await Register(req);
}
}
private Task<IActionResult> Register(SignupRequest req, bool flagForReview = false)
{
// ... your real signup logic ...
return Task.FromResult<IActionResult>(Ok(new { flagged = flagForReview }));
}
}
public record SignupRequest(string Email);- undeliverable — mailbox or domain can’t receive mail. Block it; it will hard-bounce.
- deliverable — real, reachable mailbox. Allow it.
- risky — reachable but catch-all, role-based (
info@,sales@) or disposable. Allow, optionally flag. - unknown — inconclusive (greylisting, timeout, temporary unreachability). Fail open and allow. Blocking on
unknownpenalises real users for a transient network condition.
For the deeper rationale behind treating each status this way at a signup form, see verify emails at signup without hurting conversion.
Test it without spending credits
Any address at @test.verifyany.email returns a deterministic result and costs 0 credits — no real mailbox is probed. Wire these into your integration tests to cover every branch:
deliverable@test.verifyany.email→deliverableundeliverable@test.verifyany.email→undeliverablerisky@test.verifyany.email→riskyunknown@test.verifyany.email→unknown
You can also sanity-check any single address in the browser with the free Email Verifier.
Errors, timeouts and retries
Because verification runs a live SMTP probe, plan for the small share of slow or inconclusive calls:
- Timeout. The 10-second
HttpClient.TimeoutthrowsTaskCanceledException, which the service catches and turns intonull→ the caller fails open. - Fail open on 5xx /
429. Transient upstream errors shouldn’t block signups. For background jobs, add resilience with Microsoft.Extensions.Http.Resilience (Polly) to retry429/5xxwith exponential backoff. - Don’t retry a successful
200. Anundeliverableverdict is a real answer — retrying wastes credits. - Handle
402loudly. Out-of-credits should alert you, not silently fail every check.
To verify a whole list rather than one address at a time, submit the batch endpoint and handle its signed batch.completed webhook instead of looping — see how to process large verification batches.
Wrapping up
A typed HttpClient registered through IHttpClientFactory is the clean, testable way to verify emails in .NET: configure the base address, timeout and Bearer key once, deserialize into a record, and branch on result.Status. Block only undeliverable, fail open on unknown and errors, keep the key in configuration, and test every path against @test.verifyany.email for free. The same server-side approach appears in Next.js and Java & Spring Boot; the full reference is in the API docs.