LetMeSendEmail 0.1.0

dotnet add package LetMeSendEmail --version 0.1.0
                    
NuGet\Install-Package LetMeSendEmail -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LetMeSendEmail" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LetMeSendEmail" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="LetMeSendEmail" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LetMeSendEmail --version 0.1.0
                    
#r "nuget: LetMeSendEmail, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LetMeSendEmail@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LetMeSendEmail&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=LetMeSendEmail&version=0.1.0
                    
Install as a Cake Tool

letmesend.email .NET SDK

The official .NET SDK for the letmesend.email API.

Installation

dotnet add package LetMeSendEmail

Quick Start

using LetMeSendEmail;
using LetMeSendEmail.Models;

var apiKey = Environment.GetEnvironmentVariable("LETMESENDEMAIL_API_KEY")
    ?? throw new InvalidOperationException("LETMESENDEMAIL_API_KEY is not set.");

using var client = new LetMeSendEmailClient(apiKey);

try
{
    var email = await client.Emails.SendAsync(new SendEmailRequest
    {
        From = "Acme <hello@acme.com>",
        To = new List<string> { "person@example.com" },
        Subject = "Welcome",
        Html = "<p>Hello</p>",
    });

    Console.WriteLine(email.Id);
}
catch (LetMeSendEmailException ex)
{
    Console.WriteLine($"API error ({ex.StatusCode}): {ex.Message}");
}

Configuration

var config = new LetMeSendEmailConfig(apiKey)
{
    BaseUrl = "https://letmesend.email/api/v1",
    TimeoutMs = 15_000,
    Retries = 2,
};
var client = new LetMeSendEmailClient(config);
Option Default Description
ApiKey - Your API key from the dashboard (required).
BaseUrl https://letmesend.email/api/v1 API base URL override.
TimeoutMs 30000 Complete-operation timeout in milliseconds per attempt. Applies to internally and externally supplied clients. Defaults to 30000 when zero or negative.
Retries 0 Number of automatic retries for transient failures. Clamped to 0-20.

Retries

  • Safe methods (GET, HEAD, OPTIONS, DELETE) are eligible for retries when Retries > 0.
  • Write methods (POST, PUT, PATCH) retry only when an Idempotency-Key header is present.
  • Verification endpoints (/emails/verify, /domains/verify) are never retried.
  • 429 uses exact Retry-After delay when > 0 and ⇐ 300 (no jitter, no backoff).
  • 408, 500, 502, 503, 504 use bounded exponential backoff with jitter capped at 300s.
  • Network errors and timeouts use bounded exponential backoff with jitter capped at 300s.
  • User cancellation (CancellationToken) is never retried.
  • MaxRetries is 20; configured Retries is clamped to this value.

Idempotency

var request = new SendEmailRequest
{
    From = "Acme <hello@acme.com>",
    To = new List<string> { "person@example.com" },
    Subject = "Welcome",
    Html = "<p>Hello</p>",
    IdempotencyKey = Guid.NewGuid().ToString(),
};

Dependency Injection

using LetMeSendEmail;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
var apiKey = Environment.GetEnvironmentVariable("LETMESENDEMAIL_API_KEY")
    ?? throw new InvalidOperationException("LETMESENDEMAIL_API_KEY is not set.");

services.AddSingleton(new LetMeSendEmailConfig(apiKey));

services.AddHttpClient<LetMeSendEmailClient>(http =>
{
    http.Timeout = TimeSpan.FromSeconds(15);
});

AddHttpClient creates and manages the HttpClient via IHttpClientFactory. The SDK does not dispose an externally supplied HttpClient. Config.TimeoutMs applies as the primary per-attempt bounded deadline for the complete operation, covering both internally and externally supplied clients. HttpClient.Timeout may impose an additional shorter timeout. ASP.NET Core projects already have Microsoft.Extensions.Http available. Non-ASP.NET consumers may need to add the Microsoft.Extensions.Http NuGet package.

Cancellation

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var email = await client.Emails.SendAsync(request, cts.Token);

Emails

Send (with attachments)

using LetMeSendEmail.Models;

var fileBytes = "sample PDF content"u8.ToArray();

var result = await client.Emails.SendAsync(new SendEmailRequest
{
    From = "Acme <hello@acme.com>",
    To = new List<string> { "person@example.com" },
    Subject = "Invoice",
    Html = "<p>Please find attached</p>",
    Attachments = new List<SendAttachment>
    {
        new()
        {
            Name = "invoice.pdf",
            Mime = "application/pdf",
            Content = Convert.ToBase64String(fileBytes),
            ContentDisposition = "attachment",
        },
    },
});

Send with Template

var result = await client.Emails.SendWithTemplateAsync(new SendWithTemplateRequest
{
    From = "Acme <hello@acme.com>",
    To = new List<string> { "person@example.com" },
    Subject = "Welcome",
    TemplateId = "tmpl_abc123",
    TemplateVariables = new List<TemplateVariable>
    {
        new() { Key = "USER_NAME", Type = "string", Value = "John" },
    },
});

Verify Email Address

var result = await client.Emails.VerifyAsync("person@example.com");
Console.WriteLine($"Score: {result.Score}, Status: {result.Status}");

List Emails

var page = await client.Emails.ListAsync(perPage: 20);
if (page.Data.Count > 0)
{
    var lastId = page.Data.Last().Id;
    var nextPage = await client.Emails.ListAsync(perPage: 20, after: lastId);
}

Get Email Details

var email = await client.Emails.GetAsync("email_abc123");
Console.WriteLine($"Status: {email.Status}, Recipients: {email.RecipientsCount}");

Domains

var domains = await client.Domains.ListAsync();
var domain = await client.Domains.GetAsync("domain_abc123");
var result = await client.Domains.VerifyAsync("example.com");

Contacts

using LetMeSendEmail.Models;

var contact = await client.Contacts.CreateAsync(new CreateContactRequest
{
    Email = "john@example.com",
    FirstName = "John",
    LastName = "Doe",
    Categories = new List<string> { "cat_abc123" },
});

var contactsPage = await client.Contacts.ListAsync(perPage: 20);
var fetchedContact = await client.Contacts.GetAsync("contact_abc123");

await client.Contacts.UpdateAsync("contact_abc123", new UpdateContactRequest
{
    FirstName = "Jane",
    SyncCategories = true,
});

await client.Contacts.DeleteAsync("contact_abc123");

Contact Categories

var createdCategory = await client.ContactCategories.CreateAsync("VIP", "vip");
var categoriesList = await client.ContactCategories.ListAsync();
var fetchedCategory = await client.ContactCategories.GetAsync("cat_abc123");
var updatedCategory = await client.ContactCategories.UpdateAsync("cat_abc123", "Premium", "premium");
await client.ContactCategories.DeleteAsync("cat_abc123");

Email Topics

using LetMeSendEmail.Models;

var createdTopic = await client.EmailTopics.CreateAsync(new CreateEmailTopicRequest
{
    Name = "Newsletter",
    Slug = "newsletter",
    AutoSubscribe = true,
    Domain = new EmailTopicDomainRef { Id = "domain_abc123" },
});

var topicsList = await client.EmailTopics.ListAsync();
var fetchedTopic = await client.EmailTopics.GetAsync("topic_abc123");

await client.EmailTopics.UpdateAsync("topic_abc123", new UpdateEmailTopicRequest
{
    Name = "Monthly Newsletter",
});

await client.EmailTopics.DeleteAsync("topic_abc123");

Pagination

// First page
var firstPage = await client.Emails.ListAsync(perPage: 10);
Console.WriteLine($"Has more: {firstPage.Pagination.HasMore}");
Console.WriteLine($"Total: {firstPage.Pagination.Total}");

// Next page — only when data exists and more results are available
if (firstPage.Pagination.HasMore && firstPage.Data.Count > 0)
{
    var lastId = firstPage.Data.Last().Id;
    var nextPage = await client.Emails.ListAsync(perPage: 10, after: lastId);
    Console.WriteLine($"Next page items: {nextPage.Data.Count}");
}

// Previous page — only when data exists
if (firstPage.Data.Count > 0)
{
    var firstId = firstPage.Data.First().Id;
    var prevPage = await client.Emails.ListAsync(perPage: 10, before: firstId);
    Console.WriteLine($"Previous page items: {prevPage.Data.Count}");
}

// Safe empty-results handling
var emptyPage = await client.Emails.ListAsync(perPage: 10, after: "nonexistent");
if (emptyPage.Data.Count > 0)
{
    Console.WriteLine(emptyPage.Data.First().Id);
}

// Do NOT pass after and before together — throws ArgumentException
// Do NOT pass "after: null" as a next-page request — omit after entirely
// There is no "hasPrevious" cursor; use before with the first item's ID instead

Error Handling

Exception HTTP Status When
AuthenticationError 401 Invalid or missing API key.
AuthorizationError 403 Insufficient permissions.
ValidationError 400, 413, 422 Request validation failed.
NotFoundError 404 Resource not found.
ConflictError 409 Resource conflict.
RateLimitError 429 Rate limit exceeded.
ApiError 200, 500-599 Server or malformed success response.
NetworkError - Network connectivity failure.
TimeoutError - Request timed out.
try
{
    var result = await client.Emails.SendAsync(request);
}
catch (ValidationError ex)
{
    foreach (var (field, errors) in ex.ValidationErrors)
        Console.WriteLine($"{field}: {string.Join(", ", errors)}");
}
catch (RateLimitError ex)
{
    Console.WriteLine($"Retry after {ex.RetryAfter}s");
}
catch (LetMeSendEmailException ex)
{
    Console.WriteLine($"API error ({ex.StatusCode}): {ex.Message}");
}

Each exception exposes StatusCode, ApiCode, ValidationErrors, RequestId, ResponseHeaders, and RawBody.

Webhooks

using LetMeSendEmail;

var secret = Environment.GetEnvironmentVariable("LETMESENDEMAIL_WEBHOOK_SECRET");
if (string.IsNullOrWhiteSpace(secret))
    throw new InvalidOperationException("LETMESENDEMAIL_WEBHOOK_SECRET is not set.");

// Read the raw request body
var payload = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
var headers = httpContext.Request.Headers
    .ToDictionary(h => h.Key, h => h.Value.ToString());

try
{
    var verifiedEvent = WebhookSignature.Verify(payload, headers, secret);

    // Access fields from the verified payload generically
    foreach (var (key, value) in verifiedEvent)
    {
        Console.WriteLine($"{key}: {value}");
    }
}
catch (WebhookVerificationException)
{
    httpContext.Response.StatusCode = 401;
}
catch (WebhookSigningException)
{
    httpContext.Response.StatusCode = 401;
}

Custom Tolerance

WebhookSignature.Verify(payload, headers, secret, tolerance: 600);

Required headers: webhook-id, webhook-log-id, webhook-timestamp, webhook-signature. Headers are matched case-insensitively. Both webhook-id and HTTP_WEBHOOK_ID are accepted.

Model Serialization

Every public SDK model implements ILetMeSendEmailModel and provides a ToDictionary() extension method for database-friendly plain dictionary conversion. Standard System.Text.Json serialization also works directly.

using System.Text.Json;
using LetMeSendEmail.Models;

var email = await client.Emails.GetAsync("email_abc123");

// Standard JSON serialization
var json = JsonSerializer.Serialize(email);

// ToDictionary for database storage
var dict = email.ToDictionary();
await SaveRecordAsync("emails", dict);

static Task SaveRecordAsync(string table, IReadOnlyDictionary<string, object?> record)
{
    // Application-owned database implementation
    return Task.CompletedTask;
}

See examples/model-serialization for a complete runnable example, and docs/docs.md for the full manual.

Testing

dotnet test

Version Support

.NET Supported
8.0 Yes
9.0 Yes
10.0 Yes

Full Documentation

See docs/docs.md for the complete user manual.

Changelog

See CHANGELOG.md.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 116 7/13/2026