CamelMailer 0.2.1

dotnet add package CamelMailer --version 0.2.1
                    
NuGet\Install-Package CamelMailer -Version 0.2.1
                    
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="CamelMailer" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CamelMailer" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="CamelMailer" />
                    
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 CamelMailer --version 0.2.1
                    
#r "nuget: CamelMailer, 0.2.1"
                    
#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 CamelMailer@0.2.1
                    
#: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=CamelMailer&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=CamelMailer&version=0.2.1
                    
Install as a Cake Tool

Camelmailer .NET SDK

CI NuGet License: MIT

The official .NET SDK for Camelmailer — the open-source transactional email platform.

Install

dotnet add package CamelMailer

Requires .NET 8 or later.

Quickstart

using CamelMailer;

using var camelmailer = new CamelMailerClient("cm_xxxx");

var result = await camelmailer.Emails.SendAsync(new SendEmailRequest
{
    From = "billing@acme.com",
    To = ["ada@example.com"],
    Subject = "Your receipt",
    HtmlBody = "<p>Thanks for your purchase.</p>",
});

Console.WriteLine($"Queued message {result.MessageId}");

ASP.NET Core / dependency injection

// Program.cs
builder.Services.AddCamelMailer(options =>
{
    options.ApiKey = builder.Configuration["CamelMailer:ApiKey"]!;
    // options.BaseUrl = "https://mail.example.com"; // self-hosted
});
public class ReceiptService(ICamelMailerClient camelmailer)
{
    public Task SendReceiptAsync(string to, CancellationToken cancellationToken) =>
        camelmailer.Emails.SendAsync(new SendEmailRequest
        {
            From = "billing@acme.com",
            To = [to],
            Subject = "Your receipt",
            TextBody = "Thanks for your purchase.",
        }, cancellationToken);
}

AddCamelMailer returns the underlying IHttpClientBuilder, so you can attach resilience handlers, timeouts or a custom primary handler.

Emails

// Named addresses, attachments, metadata
await camelmailer.Emails.SendAsync(new SendEmailRequest
{
    From = new EmailAddress { Email = "billing@acme.com", Name = "Acme Billing" },
    To = ["ada@example.com"],
    Subject = "Invoice #1017",
    HtmlBody = "<p>Attached.</p>",
    Attachments = [Attachment.FromBytes("invoice.pdf", "application/pdf", pdfBytes)],
    Metadata = new Dictionary<string, object?> { ["order_id"] = "o_123" },
    Tag = "invoice",
});

// Batch (one result per entry — a failed entry does not fail the batch)
var results = await camelmailer.Emails.SendBatchAsync([request1, request2]);
foreach (var entry in results.Where(r => !r.IsSuccess))
    Console.WriteLine($"{entry.Error!.Code}: {entry.Error.Message}");

// Stored templates
await camelmailer.Emails.SendWithTemplateAsync(new SendTemplateEmailRequest
{
    From = "hello@acme.com",
    To = ["ada@example.com"],
    Template = "welcome",
    TemplateModel = new Dictionary<string, object?> { ["name"] = "Ada" },
});

// Retry-safe sends: the same key with the same body returns the first result
// instead of queuing a second copy, and a different body under the same key is
// refused with InvalidIdempotentRequest. All four send methods take one.
await camelmailer.Emails.SendAsync(request, idempotencyKey: $"order-{orderId}");

// Broadcast to everyone subscribed to a stream. Recipients past the
// per-request cap of 1000 come back as Skipped, so a larger audience wants a
// campaign.
var broadcast = await camelmailer.Emails.SendToStreamAsync("newsletter", new SendToStreamRequest
{
    From = "news@acme.com",
    Subject = "September",
    TextBody = "What shipped this month.",
});

// Inspect messages
var page = await camelmailer.Emails.ListAsync(new ListEmailsOptions { Tag = "invoice" });
var details = await camelmailer.Emails.GetAsync(page.Messages[0].Id);
var opens = await camelmailer.Emails.GetOpensAsync(details.Message.Id);
var raw = await camelmailer.Emails.GetRawAsync(details.Message.Id);

Templates

var template = await camelmailer.Templates.CreateAsync(new CreateTemplateRequest
{
    Name = "Welcome",
    Subject = "Welcome, {{ name }}!",
    HtmlBody = "<p>Hi {{ name }}</p>",
});

var rendered = await camelmailer.Templates.RenderAsync(
    template.Permalink!,
    new Dictionary<string, object?> { ["name"] = "Ada" });

await camelmailer.Templates.UpdateAsync(template.Permalink!,
    new UpdateTemplateRequest { Subject = "Hello, {{ name }}!" });
await camelmailer.Templates.ArchiveAsync(template.Permalink!);

Streams

var stream = await camelmailer.Streams.CreateAsync(new CreateStreamRequest
{
    Name = "Broadcasts",
    Permalink = "broadcasts",   // the API derives one from the name when unset
    StreamType = "broadcast",
});

await camelmailer.Emails.SendAsync(new SendEmailRequest
{
    From = "news@acme.com",
    To = ["ada@example.com"],
    Subject = "June update",
    TextBody = "...",
    Stream = stream.Permalink,
});

Campaigns

A campaign is content plus an audience. The two ways to create one behave differently, so pick deliberately: CreateDraftAsync writes it and waits, CreateAndSendAsync expands it to the stream's subscribers before the call returns.

// Write it and leave it alone. Without a schedule it stays a draft; with one
// it becomes "scheduled" and the server sends it when due.
var draft = await camelmailer.Campaigns.CreateDraftAsync(new CreateDraftCampaignRequest
{
    Stream = "newsletter",
    From = "news@acme.com",
    Name = "September",
    Subject = "What shipped",
    TextBody = "Hello.",
    // ScheduledAt = DateTimeOffset.Parse("2026-10-01T08:00:00Z"),
});

// Goes out on the spot, no draft and no schedule.
await camelmailer.Campaigns.CreateAndSendAsync("newsletter", new CreateAndSendCampaignRequest
{
    Name = "Status update",
    From = "news@acme.com",
    TextBody = "All clear.",
});

var detail = await camelmailer.Campaigns.GetAsync(draft.Id);   // campaign + stats

// ScheduledAt schedules; ClearSchedule drops it back to a draft. Setting
// neither leaves the schedule standing, so the two are separate.
await camelmailer.Campaigns.UpdateAsync(draft.Id, new UpdateCampaignRequest
{
    ScheduledAt = DateTimeOffset.Parse("2026-10-01T08:00:00Z"),
});
await camelmailer.Campaigns.UpdateAsync(draft.Id, new UpdateCampaignRequest
{
    ClearSchedule = true,
});

await camelmailer.Campaigns.SendAsync(draft.Id);     // now, whatever the schedule said
await camelmailer.Campaigns.CancelAsync(draft.Id);

Subscribers

A broadcast send to an address that is not subscribed is refused, so this list is the audience.

await camelmailer.Subscribers.ListAsync("newsletter");
await camelmailer.Subscribers.AddAsync("newsletter", new AddSubscriberRequest
{
    Address = "ada@example.com",
});
await camelmailer.Subscribers.ImportAsync("newsletter", ["ada@example.com", "grace@example.com"]);
await camelmailer.Subscribers.ComplaintAsync("newsletter", "ada@example.com");  // suppress + unsubscribe
await camelmailer.Subscribers.RemoveAsync("newsletter", "ada@example.com");

Layouts

A layout wraps every template that uses it. HtmlWrapper has to embed the body with {{{ content }}}.

await camelmailer.Layouts.CreateAsync(new CreateLayoutRequest
{
    Name = "Default",
    Permalink = "default",
    HtmlWrapper = "<html><body>{{{ content }}}</body></html>",
});
var logo = await camelmailer.Layouts.UploadLogoAsync("default", "data:image/png;base64,...");
await camelmailer.Layouts.DeleteAsync("default");

Inbound and held messages

var held = await camelmailer.Inbound.ListAsync(new ListInboundOptions { Status = "held" });
await camelmailer.Inbound.RetryAsync(55);    // back on the delivery queue (.Requeued)
await camelmailer.Inbound.BypassAsync(55);   // release past the hold

Logs

Useful when a send did not arrive and the question is whether the request ever reached the API.

var requests = await camelmailer.Logs.ListAsync(perPage: 25);
var tags = await camelmailer.Logs.GetTagsAsync();

Stats, bounces and DMARC

var stats = await camelmailer.Stats.GetAsync(
    from: DateTimeOffset.UtcNow.AddDays(-7),
    to: DateTimeOffset.UtcNow);
Console.WriteLine($"{stats.Sent} sent, {stats.Bounced} bounced");

var queue = await camelmailer.Stats.GetDeliveriesAsync();
var bounces = await camelmailer.Bounces.ListAsync(new ListBouncesOptions { Tag = "invoice" });

var dmarc = await camelmailer.Dmarc.GetSummaryAsync(new DmarcQueryOptions { Domain = "acme.com" });
Console.WriteLine($"DMARC pass rate: {dmarc.PassRate:P1}");

Error handling

The SDK throws typed exceptions:

try
{
    await camelmailer.Emails.SendAsync(request);
}
catch (CamelMailerNetworkException ex)
{
    // DNS failure, refused connection, timeout — no API response received
    Console.WriteLine($"Network problem: {ex.InnerException?.Message}");
}
catch (CamelMailerException ex)
{
    // The API answered with an error envelope
    Console.WriteLine($"{ex.ErrorCode} (HTTP {(int?)ex.StatusCode}): {ex.Message}");
}

ErrorCode carries the API's stable error code (Unauthorized, ValidationError, NotFound, …); StatusCode the HTTP status.

Self-hosted instances

The client talks to the Camelmailer cloud (https://app.camelmailer.com) by default. Point it at your own installation:

using var camelmailer = new CamelMailerClient("cm_xxxx", "https://mail.example.com");

Documentation

Full API documentation: camelmailer.com/docs

License

MIT

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 was computed.  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 was computed.  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.

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.2.1 118 9/14/2026
0.2.0 101 9/14/2026
0.1.0 108 9/13/2026