Skip to content
Docs

Tutorial

Send email from ASP.NET Core and .NET with the Emailit NuGet package, MailKit or System.Net.Mail over SMTP, and verify Emailit webhooks.

Updated Oct 1, 2026

This guide shows how to send email from a .NET app with the official Emailit NuGet package, how to use MailKit or System.Net.Mail over SMTP instead, and how to verify webhooks in an ASP.NET Core minimal API.

Prerequisites

  • .NET 8 or later.
  • A verified sending domain, for example acme.com.
  • An API key. A Sending Only key restricted to your domain is enough.
  • Until your workspace has production access, you can only send to the account emails of workspace members.

Install the SDK

Terminal
dotnet add package Emailit

Configure your API key

ASP.NET Core reads configuration from environment variables, where __ separates sections. Set Emailit__ApiKey in production:

Terminal
export Emailit__ApiKey=secret_••••••••••••••••••••••••••••••••

During development, keep the key in user secrets instead of appsettings.json:

Terminal
dotnet user-secrets init
dotnet user-secrets set "Emailit:ApiKey" "secret_••••••••••••••••••••••••••••••••"

Send an email

Register one client and inject it into your endpoints:

Program.cs
using Emailit;
using Emailit.Options;
using Emailit.Resources;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(new EmailitClient(builder.Configuration["Emailit:ApiKey"]!));

var app = builder.Build();

app.MapPost("/orders/{id}/confirm", (string id, EmailitClient emailit) =>
{
    Email email = emailit.Emails.Send(new EmailSendOptions
    {
        From = "Acme <orders@acme.com>",
        To = new[] { "ada@example.com" },
        Subject = $"Order {id} confirmed",
        Html = $"<p>We've received order {id}.</p>",
    });

    return Results.Ok(new { emailId = email.Id });
});

app.Run();

Send is synchronous; for bulk sends, run it from a background service so request threads stay free. The options also accept Cc, Bcc, ReplyTo, Attachments, Template with Variables, ScheduledAt and Tracking; see Send an email.

If your code already builds System.Net.Mail.MailMessage objects, the SDK can send them through the API:

C#
using System.Net.Mail;

var message = new MailMessage("orders@acme.com", "ada@example.com", "Order confirmed", "<p>Thanks!</p>")
{
    IsBodyHtml = true,
};

Email email = emailit.Emails.Send(message);

Handle errors

C#
using Emailit.Exceptions;

try
{
    emailit.Emails.Send(options);
}
catch (RateLimitException)
{
    // 429: back off and retry
}
catch (AuthenticationException)
{
    // 401: the API key is missing or invalid
}
catch (UnprocessableEntityException ex)
{
    // 422: for example, the from domain isn't verified
    logger.LogWarning("Emailit rejected the email: {Message}", ex.Message);
}
catch (ApiErrorException ex)
{
    logger.LogError("Emailit error {Status}: {Body}", ex.HttpStatus, ex.HttpBody);
}

Send with SMTP instead

MailKit

Microsoft recommends MailKit for new SMTP code. Install it with dotnet add package MailKit:

C#
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;

var message = new MimeMessage();
message.From.Add(new MailboxAddress("Acme", "orders@acme.com"));
message.To.Add(MailboxAddress.Parse("ada@example.com"));
message.Subject = "Order confirmed";
message.Body = new TextPart("html") { Text = "<p>We've received your order.</p>" };

using var smtp = new SmtpClient();
await smtp.ConnectAsync("smtp.emailit.com", 587, SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync("emailit", builder.Configuration["Emailit:ApiKey"]);
await smtp.SendAsync(message);
await smtp.DisconnectAsync(true);

For implicit TLS, connect to port 465 with SecureSocketOptions.SslOnConnect.

System.Net.Mail

The built-in client works for simple cases. It only supports STARTTLS, so use port 587 (or 2525 or 2587):

C#
using System.Net;
using System.Net.Mail;

using var client = new SmtpClient("smtp.emailit.com", 587)
{
    EnableSsl = true,
    Credentials = new NetworkCredential("emailit", apiKey),
};

client.Send(new MailMessage("orders@acme.com", "ada@example.com", "Order confirmed", "We've received your order."));

See SMTP settings for every port.

Receive webhooks

Create a webhook and store its signing secret as Emailit:WebhookSecret. Read the raw body, verify it, then parse the array of events:

Program.cs
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

app.MapPost("/webhooks/emailit", async (HttpRequest request, IConfiguration config) =>
{
    using var reader = new StreamReader(request.Body, Encoding.UTF8);
    var body = await reader.ReadToEndAsync();

    var signature = request.Headers["X-Emailit-Signature"].ToString();
    var timestamp = request.Headers["X-Emailit-Timestamp"].ToString();

    if (!long.TryParse(timestamp, out var ts) ||
        Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
    {
        return Results.Unauthorized();
    }

    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(config["Emailit:WebhookSecret"]!));
    var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"))).ToLowerInvariant();

    if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature)))
    {
        return Results.Unauthorized();
    }

    // The body is a JSON array of up to 100 events.
    using var events = JsonDocument.Parse(body);
    foreach (var evt in events.RootElement.EnumerateArray())
    {
        if (evt.GetProperty("type").GetString() == "email.bounced")
        {
            var address = evt.GetProperty("data").GetProperty("object").GetProperty("to").GetString();
            // Stop emailing this address.
        }
    }

    return Results.Ok();
});

Return a 2xx within 30 seconds; other responses are retried. See Request signature.

Production tips

  • Register the client as a singleton. One EmailitClient per app is enough.
  • Queue bulk mail. Send from a BackgroundService or a job library such as Hangfire, and throttle it to stay under your sending limits (2 emails per second by default).
  • Make retries safe. If you retry after a timeout, send an Idempotency-Key header with HttpClient; see Idempotency.
  • Deduplicate webhooks by storing each event_id you process.

Next steps

Attachments, scheduling and tracking.
Every event and its payload.
Fix authentication and connection errors.
All official libraries.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.