LetMeSendEmail 0.1.0
dotnet add package LetMeSendEmail --version 0.1.0
NuGet\Install-Package LetMeSendEmail -Version 0.1.0
<PackageReference Include="LetMeSendEmail" Version="0.1.0" />
<PackageVersion Include="LetMeSendEmail" Version="0.1.0" />
<PackageReference Include="LetMeSendEmail" />
paket add LetMeSendEmail --version 0.1.0
#r "nuget: LetMeSendEmail, 0.1.0"
#:package LetMeSendEmail@0.1.0
#addin nuget:?package=LetMeSendEmail&version=0.1.0
#tool nuget:?package=LetMeSendEmail&version=0.1.0
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-Keyheader is present. - Verification endpoints (
/emails/verify,/domains/verify) are never retried. - 429 uses exact
Retry-Afterdelay 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. MaxRetriesis 20; configuredRetriesis 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 | Versions 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. |
-
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 |