# 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**

```bash
dotnet new console -n AveltoQuickstart && cd AveltoQuickstart
```

## 2. Create an API key

[Sign in](/login), 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**

```bash
export AVELTO_API_KEY=av_test_...
```

> **Sandbox rules.** Test keys never deliver anything; they run the pipeline and record events. The sandbox sender `you@sandbox.avelto.dev` only delivers to your account's verified owner email and to the simulator addresses `delivered@`, `bounced@` and `complained@sandbox.avelto.dev`. Anything else is refused with `403 sandbox_recipient_not_allowed`. To send to anyone, verify a domain (step 5).

## 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#**

```csharp
// Program.cs
using 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**

```bash
dotnet run
```

The API answers `201 Created` with the email id:

```http
HTTP/1.1 201 Created
Content-Type: application/json

{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }
```

> **Retrying safely.** Send an `Idempotency-Key` header (any unique string, such as your order id) with every `POST /v1/emails`. If the request times out or comes back `429`, `502`, `503` or `504`, wait a moment and send it again unchanged with the same key: the API returns the original email id instead of sending twice. See [Idempotency](/docs/send-email).

## 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#**

```csharp
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 (three DKIM CNAMEs, an SPF TXT and a DMARC TXT), 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#**

```csharp
var created = await http.PostAsJsonAsync(
    "/v1/domains", new { name = "mail.acme.com" });
created.EnsureSuccessStatusCode();
var domain = await created.Content.ReadFromJsonAsync<JsonElement>();

foreach (var r in domain.GetProperty("dns_records").EnumerateArray())
{
    Console.WriteLine($"{r.GetProperty("type")}\t{r.GetProperty("name")}\t" +
        $"{r.GetProperty("value")}\t({r.GetProperty("purpose")})");
}

// Publish the records, then poll. GET re-checks DNS on every call.
var domainId = domain.GetProperty("id").GetString();
var status = domain.GetProperty("status").GetString();
while (status == "pending")
{
    await Task.Delay(TimeSpan.FromSeconds(30));
    var current = await http.GetFromJsonAsync<JsonElement>($"/v1/domains/{domainId}");
    status = current.GetProperty("status").GetString();
}
Console.WriteLine(status); // "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](/docs/send-email): every field, attachments, tags, scheduling and idempotency.
- [Webhooks](/docs/webhooks): get events pushed to your app.
- [Test mode](/docs/test-mode): test keys, the sandbox sender and the simulator addresses.

---

Rendered page: https://avelto.dev/docs/quickstart/dotnet
