← Back to all posts
2026-08-30 · MailAnvil Team

Send Transactional Email with .NET 8 + MailAnvil — Minimal API Tutorial

.NET shops don't need an SMTP library to send transactional email. MailAnvil is a plain HTTPS API, which means you can send from any .NET app with the HttpClient already in the base class library — no MailKit, no SmtpClient, no NuGet package at all.

This tutorial builds a .NET 8 minimal API endpoint that sends a welcome email when a user signs up.

What you'll build

A POST /signup endpoint that:

  1. Accepts an email address
  2. Calls MailAnvil's POST /v1/send in the background
  3. Returns immediately — the user never waits on email delivery

Prerequisites

Step 1 — Scaffold the project

dotnet new web -n AcmeApi
cd AcmeApi

This gives you a minimal API project with Program.cs — no controllers, no boilerplate.

Step 2 — A typed MailAnvil client

Create MailAnvilClient.cs. It wraps one endpoint in a strongly-typed method so the rest of your app never touches raw JSON:

using System.Text;
using System.Text.Json;

public sealed class MailAnvilClient
{
    private readonly HttpClient _http;
    private readonly string _apiKey;

    public MailAnvilClient(HttpClient http, IConfiguration config)
    {
        _http = http;
        _apiKey = config["MailAnvil:ApiKey"]
            ?? throw new InvalidOperationException("MailAnvil:ApiKey not set");
    }

    public async Task SendAsync(
        string to,
        string subject,
        string text,
        string html,
        string idempotencyKey,
        CancellationToken ct = default)
    {
        var payload = new
        {
            from = "[email protected]",
            to = new[] { to },
            subject,
            text,
            html
        };

        using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.mailanvil.com/v1/send");
        req.Headers.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiKey);
        req.Headers.TryAddWithoutValidation("Idempotency-Key", idempotencyKey);
        req.Content = new StringContent(
            JsonSerializer.Serialize(payload),
            Encoding.UTF8,
            "application/json");

        using var resp = await _http.SendAsync(req, ct);
        if (!resp.IsSuccessStatusCode)
        {
            var body = await resp.Content.ReadAsStringAsync(ct);
            throw new HttpRequestException(
                $"MailAnvil returned {(int)resp.StatusCode}: {body}");
        }
    }
}

Three details worth noticing:

Step 3 — Register it and add the endpoint

Replace Program.cs:

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<MailAnvilClient>(c =>
{
    c.Timeout = TimeSpan.FromSeconds(15);
});

var app = builder.Build();

app.MapPost("/signup", async (SignupRequest body, MailAnvilClient mail) =>
{
    // Fire-and-forget: don't block the signup on email latency.
    _ = Task.Run(async () =>
    {
        try
        {
            await mail.SendAsync(
                to: body.Email,
                subject: "Welcome to Acme!",
                text: "Thanks for signing up.",
                html: "<h1>Welcome to Acme</h1><p>Thanks for signing up.</p>",
                idempotencyKey: $"signup-{body.SignupId}");
        }
        catch (Exception ex)
        {
            // Log to your sink of choice. The user already got their 202.
            Console.Error.WriteLine($"Email send failed: {ex.Message}");
        }
    });

    return Results.Accepted();
});

app.Run();

public record SignupRequest(string Email, string SignupId);

The key decision here: 202 Accepted, not a blocking send. Signup latency should be measured in milliseconds, and email delivery is asynchronous by nature. If the send fails, you log it and retry — the user isn't left staring at a spinner.

Step 4 — Configure the key

Add to appsettings.Development.json (never commit a real key):

{
  "Logging": { "LogLevel": { "Default": "Information" } },
  "MailAnvil": {
    "ApiKey": "re_key_xxxxxxxx"
  }
}

In production, inject it through the environment instead:

export MailAnvil__ApiKey="re_key_xxxxxxxx"

.NET's configuration system maps the MailAnvil__ApiKey environment variable to config["MailAnvil:ApiKey"] automatically.

Test it

dotnet run

In another terminal:

curl -X POST http://localhost:5000/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","signupId":"sig_12345"}'

You should get a 202 immediately, and the welcome email lands within seconds.

Why this matters for Indonesian .NET teams

Enterprise and government stacks in Indonesia run heavily on .NET, and those teams are often told they need an SMTP relay with its own client library. That's a legacy reflex. A modern transactional email API over HTTPS means:

Swap the base URL and API key shape and the exact same HttpClient pattern works against any provider. But the idempotency and the IDR billing are the parts that make MailAnvil a natural fit for local .NET shops.

Full picture

Piece What it does
MailAnvilClient One typed method over POST /v1/send
Idempotency-Key Safe retries without duplicate sends
AddHttpClient Connection pooling + timeout, free from the framework
202 Accepted Signup never blocks on email