MillionSend 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package MillionSend --version 0.2.0
                    
NuGet\Install-Package MillionSend -Version 0.2.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="MillionSend" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MillionSend" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="MillionSend" />
                    
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 MillionSend --version 0.2.0
                    
#r "nuget: MillionSend, 0.2.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 MillionSend@0.2.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=MillionSend&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=MillionSend&version=0.2.0
                    
Install as a Cake Tool

MillionSend .NET SDK

Official .NET SDK for MillionSend — a self-hostable, Resend-wire-compatible email API.

The API is wire-compatible with Resend, so migrating is mostly swapping the package and pointing the client at your instance. Targets net8.0.

Install

dotnet add package MillionSend

Quickstart

using MillionSend;

var client = new MillionSendClient("ms_123", "https://mail.acme.dev");

var res = await client.EmailSendAsync(new EmailMessage
{
    From = "Acme <onboarding@acme.dev>",
    To = "delivered@resend.dev",
    Subject = "Hello from MillionSend",
    Html = "<strong>It works!</strong>",
});

if (res.Success)
    Console.WriteLine($"sent {res.Content!.Id}");
else
    Console.Error.WriteLine($"{res.Exception!.ErrorName}: {res.Exception.Message}");

To, Cc, Bcc, and ReplyTo accept a single address or an array — assign a string or a string[] directly.

Configuration

Construct with the convenience constructor, or with MillionSendClientOptions:

var client = new MillionSendClient(new MillionSendClientOptions
{
    ApiToken = "ms_123",
    ApiUrl = "https://mail.acme.dev",
});
  • ApiToken falls back to the MILLIONSEND_API_KEY environment variable. Missing key → throws at construction.
  • ApiUrl falls back to MILLIONSEND_BASE_URL, then http://localhost:3001. MillionSend is self-hosted, so set this to your deployment in production.

You may pass your own HttpClient as the second argument (for proxies, custom handlers, or tests): new MillionSendClient(options, httpClient).

Error handling

No method throws for an API or transport error. Every call returns a MillionSendResponse<T>:

  • Successtrue when the call succeeded.
  • Content — the deserialized response body on success.
  • Exception — a MillionSendException on failure, carrying StatusCode (int?), ErrorName (the stable snake_case discriminant, e.g. validation_error, not_found, sending_paused), and Message.

Transport and client-side failures (the request never reached the API) carry StatusCode == null and ErrorName == "application_error".

var res = await client.EmailRetrieveAsync(id);
if (!res.Success && res.Exception!.ErrorName == "not_found")
{
    // ...
}

Resources

Emails

await client.EmailSendAsync(message, idempotencyKey: "unique-key"); // POST /emails
await client.EmailRetrieveAsync(id);                                // GET /emails/{id}
await client.EmailCancelAsync(id);                                  // POST /emails/{id}/cancel (scheduled only)
await client.EmailBatchAsync(new[] { messageA, messageB }, idempotencyKey: "k"); // up to 100

EmailSendAsync and EmailBatchAsync accept an optional idempotencyKey — the only two endpoints that support the Idempotency-Key header.

Contacts

Contacts are team-global: one record per email address (case-insensitive), no audiences. Creating a duplicate returns a 409 validation_error.

await client.ContactAddAsync(new ContactCreateOptions
{
    Email = "ada@acme.dev",
    FirstName = "Ada",
    Properties = new() { ["plan"] = "pro" },
});
await client.ContactRetrieveAsync(new ContactAddress { Email = "ada@acme.dev" });
await client.ContactRetrieveAsync(new ContactAddress { Id = contactId });   // by id or email (email wins)
await client.ContactUpdateAsync(new ContactUpdateOptions { Id = contactId, Unsubscribed = true });
await client.ContactDeleteAsync(new ContactAddress { Email = "ada@acme.dev" });
await client.ContactListAsync(new ListOptions { Limit = 50 });

// Topic subscriptions (granular unsubscribe)
await client.ContactTopicsUpdateAsync(new ContactTopicsUpdateOptions
{
    Email = "ada@acme.dev",
    Topics = new() { new ContactTopicUpdate { Id = topicId, Subscription = TopicSubscription.OptOut } },
});

A contact is addressable by id or email; when both are set, email wins.

Topics

await client.TopicAddAsync(new TopicCreateOptions { Name = "Product updates", DefaultSubscription = TopicSubscription.OptIn });
await client.TopicRetrieveAsync(id);
await client.TopicListAsync();     // unpaginated: bare { data }
await client.TopicDeleteAsync(id);

Broadcasts

Targeting is an optional SegmentId and/or TopicId — set neither to send to every contact of the team.

var broadcast = await client.BroadcastAddAsync(new BroadcastCreateOptions
{
    From = "Acme <news@acme.dev>",
    Subject = "Launch",
    Html = "<p>Hi {{{FIRST_NAME|there}}}</p>",
    SegmentId = segmentId, // optional
});
await client.BroadcastListAsync();
await client.BroadcastRetrieveAsync(id);
await client.BroadcastUpdateAsync(id, new BroadcastUpdateOptions { Subject = "Launch 🚀" }); // draft only
await client.BroadcastSendAsync(id, scheduledAt: "2026-09-01T09:00:00Z"); // omit to send now
await client.BroadcastCancelAsync(id); // scheduled only
await client.BroadcastDeleteAsync(id); // draft only

Segments (MillionSend extension)

Dynamic segments are a saved filter over the team's contacts — a MillionSend superset with no Resend equivalent.

await client.SegmentAddAsync(new SegmentCreateOptions
{
    Name = "Pro plan",
    Filter = new SegmentFilter
    {
        Match = SegmentMatch.All,
        Conditions = new() { new SegmentCondition { Field = "property:plan", Op = "equals", Value = "pro" } },
    },
});
await client.SegmentRetrieveAsync(id);   // includes a live contact_count
await client.SegmentListAsync();
await client.SegmentUpdateAsync(id, new SegmentUpdateOptions { Name = "Pro tier" });
await client.SegmentDeleteAsync(id);

Migrating from Resend

The resend-dotnet SDK exposes resources as client.Emails.SendAsync(...) and throws ResendException. MillionSend flattens the surface to client.EmailSendAsync(...) and returns a MillionSendResponse<T> (no throw). Method names and payload shapes otherwise line up. Notes:

  • Domains and API keys are managed in the MillionSend dashboard, not via the API, so there are no domain/api-key methods here.
  • No audiences: contacts are team-global, one record per email address. Resend's audience/segment grouping maps to MillionSend's dynamic segments (saved filters) above.

Development

dotnet test          # unit tests (the e2e tests stay inert without MILLIONSEND_E2E)

The e2e tests run only when MILLIONSEND_E2E=1 opts in (plus the API key):

MILLIONSEND_E2E=1 MILLIONSEND_API_KEY=ms_... dotnet test --filter Category=e2e

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.
  • net8.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.8.0 100 9/8/2026
0.7.0 97 9/5/2026
0.6.0 96 9/4/2026
0.5.0 94 9/4/2026
0.4.0 95 9/4/2026
0.3.0 100 8/31/2026
0.2.0 115 8/17/2026
0.1.0 111 8/16/2026
0.0.1 107 8/13/2026