← Back to blog
·8 min read·
Paulo CastellanoPaulo Castellano

How to send email with .NET (C#) using an email API

Send email from .NET apps using the Sendkit C# SDK. Covers setup, HTML email, error handling, SMTP, and ASP.NET integration.

dotnetcsharpemail-apitransactional-emailtutorial
How to send email with .NET (C#) using an email API

.NET has System.Net.Mail.SmtpClient built in, and it works. It also hasn't been meaningfully updated in years, Microsoft recommends against using it for new code, and every time you touch MailMessage you end up fighting with encoding, attachment streams, and connection pooling issues that have nothing to do with your actual app.

An email API is simpler. You make an HTTP call, get back a message ID, and move on. This guide covers sending email from .NET with the Sendkit email API, starting with the basics and working through HTML, error handling, retries, and SMTP for legacy codebases.

Install the SDK

Add the Sendkit NuGet package to your project:

dotnet add package Sendkit

You'll need an API key from your Sendkit dashboard. Keys starting with sk_live_ send real email. Keys starting with sk_test_ validate everything but don't deliver, which is what you want during development and in your test suite.

using Sendkit;

var client = new SendkitClient("sk_live_your_api_key");

Don't hardcode that key. Use environment variables, user secrets, or whatever secrets management your project already has:

using Sendkit;

// Reads the SENDKIT_API_KEY environment variable automatically
var client = new SendkitClient();

For ASP.NET apps, pull it from configuration:

var apiKey = builder.Configuration["Sendkit:ApiKey"];
var client = new SendkitClient(apiKey);

Send a plain text email

The minimum viable send:

var response = await client.Emails.SendAsync(new SendEmailParams
{
    From = "[email protected]",
    To = ["[email protected]"],
    Subject = "Your order has shipped",
    Html = "<p>Order #4821 shipped via FedEx. Tracking number: 7489201384.</p>"
});

Console.WriteLine(response.Id); // msg_8f2a...

The From address has to be on a domain you've verified in Sendkit. If you haven't done that yet, the DMARC, DKIM, and SPF setup guide covers the DNS records you need.

To takes an array, so you can send to multiple recipients in one call.

Send HTML email

Most transactional emails need formatting. Always include a plain text fallback for clients that can't render HTML.

async Task<string> SendWelcomeEmail(string userEmail, string userName)
{
    var response = await client.Emails.SendAsync(new SendEmailParams
    {
        From = "[email protected]",
        To = [userEmail],
        Subject = $"Welcome, {userName}",
        Html = $"""
        <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
            <h2>Welcome aboard, {userName}</h2>
            <p>Your account is set up and ready to go.</p>
            <p>Here are a few things to try first:</p>
            <ul>
                <li>Complete your profile</li>
                <li>Connect your first integration</li>
                <li>Invite your team</li>
            </ul>
            <a href="https://app.yoursite.com/getting-started"
               style="background: #0066ff; color: white; padding: 12px 24px;
                      text-decoration: none; border-radius: 4px;
                      display: inline-block; margin-top: 16px;">
                Get started
            </a>
        </div>
        """
    });

    return response.Id;
}

Inline CSS is the only approach that works across Gmail, Outlook, Apple Mail, and the rest of them. <style> blocks get stripped. External stylesheets get ignored. Inline styles are ugly in the source but reliable in the inbox.

Code on a computer screen

Send to multiple recipients

Pass multiple addresses in the To array:

var response = await client.Emails.SendAsync(new SendEmailParams
{
    From = "[email protected]",
    To = ["[email protected]", "[email protected]"],
    Subject = "[ALERT] Database connection pool exhausted",
    Html = "<p>Connection pool hit 100% at 14:32 UTC. Auto-scaling triggered.</p>"
});

For batch sends where each recipient gets different content, loop and send individually. The API docs cover templating if you need personalization at scale.

Before sending to any list of addresses, validate them first. Bad addresses hurt your sender reputation, and that damage affects every email you send afterward. Sendkit has built-in email validation that catches typos, disposable addresses, and dead mailboxes before you waste a send on them.

Error handling

The happy path is a few lines. The part that keeps your password reset flow from silently dying at 3am is error handling.

SendkitException gives you structured error data: a machine-readable Name, the HTTP StatusCode, and a human-readable Message.

using Sendkit;

var client = new SendkitClient();

async Task<SendResult> SendEmailSafely(SendEmailParams emailParams)
{
    try
    {
        var response = await client.Emails.SendAsync(emailParams);
        return new SendResult(true, response.Id);
    }
    catch (SendkitException ex) when (ex.StatusCode == 422)
    {
        // Bad input: invalid address, missing required field
        Console.WriteLine($"Validation error: {ex.Message}");
        return new SendResult(false, Error: ex.Message);
    }
    catch (SendkitException ex) when (ex.StatusCode == 429)
    {
        // Rate limited
        Console.WriteLine($"Rate limited: {ex.Message}");
        return new SendResult(false, Error: "rate_limited", Retryable: true);
    }
    catch (SendkitException ex) when (ex.StatusCode >= 500)
    {
        // Something broke on Sendkit's side
        Console.WriteLine($"Server error: {ex.StatusCode}");
        return new SendResult(false, Error: "server_error", Retryable: true);
    }
    catch (SendkitException ex)
    {
        Console.WriteLine($"API error ({ex.Name}): {ex.Message}");
        return new SendResult(false, Error: ex.Message);
    }
    catch (HttpRequestException ex)
    {
        // Network timeout, DNS failure, etc.
        Console.WriteLine($"Network error: {ex.Message}");
        return new SendResult(false, Error: "network_error", Retryable: true);
    }
}

record SendResult(
    bool Success,
    string? MessageId = null,
    string? Error = null,
    bool Retryable = false
);

C#'s exception filters (when) are useful here. You can match on status code without nesting if/else inside a single catch block.

For production, add retry logic with exponential backoff:

async Task<SendResult> SendWithRetry(SendEmailParams emailParams, int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        var result = await SendEmailSafely(emailParams);

        if (result.Success || !result.Retryable)
            return result;

        var delay = TimeSpan.FromSeconds(Math.Min(Math.Pow(2, attempt), 30));
        Console.WriteLine($"Attempt {attempt} failed. Retrying in {delay.TotalSeconds}s...");
        await Task.Delay(delay);
    }

    return new SendResult(false, Error: "max_retries_exceeded");
}

Three retries covers most transient failures. If it's still failing after that, push it into a background queue (Hangfire, a channel, whatever your stack uses) rather than blocking the HTTP request.

Person working on a laptop with code

Dependency injection in ASP.NET

If you're running ASP.NET, register SendkitClient as a singleton so it reuses the underlying HttpClient:

// Program.cs
builder.Services.AddSingleton<SendkitClient>(sp =>
{
    var apiKey = builder.Configuration["Sendkit:ApiKey"];
    return new SendkitClient(apiKey);
});

Then inject it wherever you need it:

public class OrderService
{
    private readonly SendkitClient _sendkit;

    public OrderService(SendkitClient sendkit)
    {
        _sendkit = sendkit;
    }

    public async Task SendOrderConfirmation(string email, int orderId)
    {
        try
        {
            await _sendkit.Emails.SendAsync(new SendEmailParams
            {
                From = "[email protected]",
                To = [email],
                Subject = $"Order #{orderId} confirmed",
                Html = $"<p>Your order #{orderId} has been confirmed and is being processed.</p>"
            });
        }
        catch (SendkitException ex)
        {
            // Log it, queue for retry, alert if critical
            Console.WriteLine($"Failed to send order email: {ex.Name} - {ex.Message}");
        }
    }
}

Keep the API key in appsettings.json for local dev and pull it from environment variables or a vault in production:

{
  "Sendkit": {
    "ApiKey": "sk_live_your_api_key"
  }
}

Using SMTP instead of the API

If your codebase already uses SmtpClient or a library that expects SMTP, you can point it at Sendkit's SMTP relay without rewriting anything:

using System.Net;
using System.Net.Mail;

using var smtpClient = new SmtpClient("smtp.sendkit.com", 587)
{
    Credentials = new NetworkCredential("sendkit", "sk_live_your_api_key"),
    EnableSsl = true
};

var message = new MailMessage
{
    From = new MailAddress("[email protected]"),
    Subject = "Password reset",
    Body = "Click here to reset your password: https://app.yoursite.com/reset?token=abc123"
};
message.To.Add("[email protected]");

await smtpClient.SendMailAsync(message);

Your API key doubles as your SMTP password. No extra credentials.

The API is the better choice for new code. No SMTP handshake overhead, you get a message ID back immediately, and error responses are structured JSON instead of SMTP reply codes you have to parse. But SMTP works fine when you need to drop Sendkit into something that already exists.

Send raw MIME email

If you're building MIME messages yourself (maybe you're migrating from another provider and already have MIME generation), the SDK supports that too:

var mimeContent = "From: [email protected]\r\nTo: [email protected]\r\n" +
                  "Subject: Raw MIME test\r\nContent-Type: text/plain\r\n\r\n" +
                  "This is a raw MIME message.";

var response = await client.Emails.SendMimeAsync(new SendMimeEmailParams
{
    EnvelopeFrom = "[email protected]",
    EnvelopeTo = "[email protected]",
    RawMessage = mimeContent
});

Most people won't need this. The standard SendAsync method is easier for almost every case.

After the email leaves your app

The code part is done in a few minutes. Whether the email reaches the inbox is a longer-term problem.

Authenticate your domain. DKIM, SPF, and DMARC are the minimum. Without them, Gmail and Outlook will either send your messages to spam or reject them outright. The setup guide walks through the DNS records.

Watch your bounces. If you keep sending to addresses that don't exist, your sender reputation drops, and that drags down delivery rates for all your email. Sendkit suppresses bounced addresses automatically, but keep an eye on your own bounce rate too.

Validate addresses before sending. If you accept email addresses from users (signup forms, invite flows), a validation call catches typos like gmial.com and throwaway addresses that will never engage. Worth the extra API call.

The Sendkit docs cover webhooks, templates, and everything else if you need to go deeper.

Wrapping up

Install Sendkit, call client.Emails.SendAsync(), handle errors, register it in DI if you're using ASP.NET. That's the sending part.

The ongoing work is deliverability: authenticate your domain, don't ignore bounces, validate addresses before sending. The code is a few lines. Keeping a clean sender reputation is what actually takes effort.

Share this article