Send email from .NET

Five steps: create a project, create a test key, send to the sandbox, read the log, then verify a domain so you can send to anyone.

1. Install

No SDK needed. The API is plain JSON over HTTPS, so HttpClient and System.Text.Json from the base class library are all it takes. Start a console project (.NET 8 or later):

shell
dotnet new console -n AveltoQuickstart && cd AveltoQuickstart

2. Create an API key

Sign in, open API keys in the dashboard and create a test key. It starts with av_test_. Export it so the program can read it (on PowerShell, $env:AVELTO_API_KEY = "av_test_..."):

shell
export AVELTO_API_KEY=av_test_...

3. Send your first email

Every request carries the key as a bearer token. A rejected send comes back as a non-2xx status with a JSON body of { "error": { "code", "message" } }.

C#
// Program.csusing System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json;var http = new HttpClient { BaseAddress = new Uri("https://api.avelto.dev") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(    "Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));var response = await http.PostAsJsonAsync("/v1/emails", new Dictionary<string, string>{    ["from"] = "you@sandbox.avelto.dev",    ["to"] = "delivered@sandbox.avelto.dev",    ["subject"] = "Hello from Avelto",    ["text"] = "It works.",});var json = await response.Content.ReadFromJsonAsync<JsonElement>();if (!response.IsSuccessStatusCode){    var error = json.GetProperty("error");    Console.Error.WriteLine($"{(int)response.StatusCode} " +        $"{error.GetProperty("code")}: {error.GetProperty("message")}");    return;}var id = json.GetProperty("id").GetString();Console.WriteLine(id); // "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10"
shell
dotnet run

The API answers 201 Created with the email id:

HTTP
HTTP/1.1 201 CreatedContent-Type: application/json{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }

4. Check the log

Fetch the email by id. status moves from queued to sent to delivered, and events records each step: email.queued, email.sent, email.delivered.

C#
var email = await http.GetFromJsonAsync<JsonElement>($"/v1/emails/{id}");// "queued", then "sent", then "delivered"Console.WriteLine(email.GetProperty("status").GetString());foreach (var e in email.GetProperty("events").EnumerateArray()){    Console.WriteLine($"{e.GetProperty("type")} {e.GetProperty("occurred_at")}");}
JSON
{  "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10",  "mode": "test",  "from": "you@sandbox.avelto.dev",  "to": ["delivered@sandbox.avelto.dev"],  "subject": "Hello from Avelto",  "status": "delivered",  "events": [    {      "id": "e1f0c3a4-8b2d-4c6e-9a1f-5d7b3e2c8a90",      "type": "email.queued",      "payload": {},      "occurred_at": "2026-09-17T10:12:04.000Z"    },    {      "id": "a7c2e9d1-3f4b-4a8e-b6c0-2d9e1f7b5c34",      "type": "email.sent",      "payload": { "test": true, "ses_message_id": "test-9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" },      "occurred_at": "2026-09-17T10:12:06.000Z"    },    {      "id": "c4b8d2f6-7e1a-4d3c-8f9b-6a2e0c5d1b78",      "type": "email.delivered",      "payload": { "test": true, "recipients": ["delivered@sandbox.avelto.dev"] },      "occurred_at": "2026-09-17T10:12:06.000Z"    }  ]}

5. Verify a domain

Add a domain, publish the DNS records it prints (an ownership TXT, an SPF TXT and a DMARC TXT, then three DKIM CNAMEs once the ownership record is found), then poll GET /v1/domains/:id until status is verified. The GET re-checks DNS on every call. Use a subdomain such as mail.acme.com.

C#
var created = await http.PostAsJsonAsync(    "/v1/domains", new { name = "mail.acme.com" });created.EnsureSuccessStatusCode();var domain = await created.Content.ReadFromJsonAsync<JsonElement>();void Show(JsonElement d){    foreach (var r in d.GetProperty("dns_records").EnumerateArray())    {        Console.WriteLine($"{r.GetProperty("type")}\t{r.GetProperty("name")}\t" +            $"{r.GetProperty("value")}\t({r.GetProperty("purpose")})");    }}Show(domain); // the ownership record first, with SPF and DMARC// Publish the records, then poll. GET re-checks DNS on every call.// Once the ownership record is found, the DKIM records appear: publish those too.var domainId = domain.GetProperty("id").GetString();while (domain.GetProperty("status").GetString() == "pending"){    await Task.Delay(TimeSpan.FromSeconds(30));    var latest = await http.GetFromJsonAsync<JsonElement>($"/v1/domains/{domainId}");    if (latest.GetProperty("ownership_verified").GetBoolean() &&        !domain.GetProperty("ownership_verified").GetBoolean())    {        Show(latest);    }    domain = latest;}Console.WriteLine(domain.GetProperty("status").GetString()); // "verified" or "failed"

Once the domain is verified, switch AVELTO_API_KEY to a live key (av_live_) and change from to an address on it, such as hello@mail.acme.com. Nothing else changes.

Next

  • Send email: every field, attachments, tags, scheduling and idempotency.
  • Webhooks: get events pushed to your app.
  • Test mode: test keys, the sandbox sender and the simulator addresses.