# C# and .NET

HttpClient and System.Text.Json, registered the way ASP.NET expects.

There is no emails.sh NuGet package. HttpClient covers the API, and the class below is what you register in the DI container of a web app or call directly from a console program.

### Send

EmailClient.cs:
```text
// Email/EmailClient.cs
using System.Net.Http.Json;
using System.Text.Json.Serialization;

namespace Acme;

public record SendEmail(
    [property: JsonPropertyName("from")] string From,
    [property: JsonPropertyName("to")] string[] To,
    [property: JsonPropertyName("subject")] string Subject,
    [property: JsonPropertyName("html")] string Html);

public record Queued(
    [property: JsonPropertyName("id")] string Id,
    [property: JsonPropertyName("status")] string Status);

public record RefusalBody(
    [property: JsonPropertyName("code")] string Code,
    [property: JsonPropertyName("message")] string Message,
    [property: JsonPropertyName("next")] string? Next);

public record Refusal([property: JsonPropertyName("error")] RefusalBody Error);

public class EmailRefusedException(Refusal refusal)
    : Exception($"{refusal.Error.Code}: {refusal.Error.Message} {refusal.Error.Next}")
{
    public string Code { get; } = refusal.Error.Code;
}

public class EmailClient(HttpClient http)
{
    public async Task<Queued> SendAsync(SendEmail email, CancellationToken ct = default)
    {
        var response = await http.PostAsJsonAsync("/v1/emails", email, ct);

        if (!response.IsSuccessStatusCode)
        {
            var refusal = await response.Content.ReadFromJsonAsync<Refusal>(ct)
                          ?? new Refusal(new RefusalBody("unknown", $"HTTP {(int)response.StatusCode}", null));
            throw new EmailRefusedException(refusal);
        }

        return (await response.Content.ReadFromJsonAsync<Queued>(ct))!;
    }
}
```

### Register it

Program.cs:
```text
// Program.cs
using Acme;

var builder = WebApplication.CreateBuilder(args);

// The key comes from https://emails.sh/dashboard/api-keys. In development put
// it in user secrets (dotnet user-secrets set "Emailssh:ApiKey" "esh_...");
// in production it is an environment variable, EMAILSSH_API_KEY.
var apiKey = builder.Configuration["Emailssh:ApiKey"]
             ?? Environment.GetEnvironmentVariable("EMAILSSH_API_KEY")
             ?? throw new InvalidOperationException("No emails.sh API key configured");

builder.Services.AddHttpClient<EmailClient>(client =>
{
    client.BaseAddress = new Uri("https://emails.sh");
    client.Timeout = TimeSpan.FromSeconds(10);
    client.DefaultRequestHeaders.Authorization = new("Bearer", apiKey);
});

var app = builder.Build();

app.MapPost("/signup", async (EmailClient emails, string address) =>
{
    var queued = await emails.SendAsync(new SendEmail(
        From: "Acme <hello@acme.com>",
        To: [address],
        Subject: "Welcome to Acme",
        Html: "<p>Confirm your address to finish signing up.</p>"));

    return Results.Ok(new { queued.Id });
});

app.Run();
```

AddHttpClient gives you one pooled HttpClient with the header already on it. Constructing a new HttpClient per send exhausts sockets under load, and it is the usual cause of a service that sends fine for an hour and then stops.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
