AxeneAxene Docs
SDKs

.NET SDK

Send email, manage domains, contacts, templates, and webhooks from .NET with the official Axene.Mailer package for C#.

The official .NET SDK is published as Axene.Mailer on NuGet. Source lives in the axene-sdks repository.

The package targets net8.0 and netstandard2.0, so it works across modern .NET, .NET Framework, Xamarin, and Unity. Every API call is async and returns a Task.

Installation

dotnet add package Axene.Mailer

You can also add it through the NuGet Package Manager or by adding a <PackageReference> to your .csproj.

Client setup

Create an API key in the dashboard under Settings -> API Keys. Keys start with axm_k_. Construct the client with that key:

using Axene.Mailer;
 
var axene = new AxeneMailerClient("axm_k_your_api_key");

The constructor accepts these parameters:

ParameterTypeDefaultDescription
apiKeystring(required)Your API key. Starts with axm_k_.
baseUrlstring?https://mail.axene.ioOverride the API base URL. A trailing slash is trimmed automatically.
httpClientHttpClient?nullSupply your own HttpClient (recommended under dependency injection). One is created and owned otherwise.
maxRetriesint3Total attempts on 429 / 5xx, including the first.
using System.Net.Http;
using Axene.Mailer;
 
var http = new HttpClient();
var axene = new AxeneMailerClient(
    apiKey: Environment.GetEnvironmentVariable("AXENE_API_KEY")!,
    baseUrl: "https://mail.axene.io",
    httpClient: http,
    maxRetries: 3);

AxeneMailerClient implements IDisposable. It disposes the underlying HttpClient only when it created the client itself. When you pass your own HttpClient (for example one managed by IHttpClientFactory), the SDK leaves its lifetime to you.

Send an email

Emails.SendAsync queues a single message and returns its id and initial status. Use an object initializer to build the SendEmail; the To collection is initialized for you, so you can add recipients directly.

using Axene.Mailer;
 
var axene = new AxeneMailerClient("axm_k_your_api_key");
 
SendEmailResult result = await axene.Emails.SendAsync(new SendEmail
{
    From = new Address("[email protected]", "Your Company"),
    To = { "[email protected]" },
    Subject = "Your receipt",
    Html = "<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
    Text = "Thanks for your order. Your receipt is attached.",
});
 
Console.WriteLine($"{result.Id} {result.Status}");

The returned SendEmailResult carries:

FieldTypeDescription
IdstringThe message id.
StatusstringInitial status, for example queued or scheduled.
MessageIdstring?RFC 5322 Message-ID, once assigned.
RejectionReasonstring?Reason the message was rejected, if it was.

A few conveniences worth knowing:

  • Address converts implicitly from a bare email string, so To = { "[email protected]" } is shorthand for To = { new Address("[email protected]") }. Use new Address(email, name) when you want a display name.
  • The SDK exposes a clean From property and maps it to the wire field from_ for you. You never write from_.
  • Provide Html, Text, or both.
  • SendAt is a DateTimeOffset?; set it to schedule the message for later (Starter plan and up). It is serialized as UTC ISO 8601.
await axene.Emails.SendAsync(new SendEmail
{
    From = "[email protected]",
    To = { new Address("[email protected]", "Ada"), "[email protected]" },
    Cc = new List<Address> { "[email protected]" },
    ReplyTo = new Address("[email protected]"),
    Subject = "Welcome aboard",
    Html = "<p>Welcome!</p>",
    Tags = new List<string> { "onboarding" },
    SendAt = DateTimeOffset.UtcNow.AddHours(1),
});

The client also exposes a shortcut axene.SendAsync(...) that forwards to Emails.SendAsync.

Emails

Accessed as client.Emails.

// Send a single email.
SendEmailResult sent = await axene.Emails.SendAsync(new SendEmail
{
    From = "[email protected]",
    To = { "[email protected]" },
    Subject = "Hello",
    Html = "<p>Hi</p>",
});
 
// Send a batch (Starter plan and up, capped by your plan).
BatchResult batch = await axene.Emails.SendBatchAsync(new[]
{
    new SendEmail { From = "[email protected]", To = { "[email protected]" }, Subject = "Hi", Html = "<p>A</p>" },
    new SendEmail { From = "[email protected]", To = { "[email protected]" }, Subject = "Hi", Html = "<p>B</p>" },
});
Console.WriteLine($"{batch.Total} {batch.Sent} {batch.Failed}");
 
// Dry-run a send without sending it.
ValidationResult check = await axene.Emails.ValidateAsync(sent: new SendEmail
{
    From = "[email protected]",
    To = { "[email protected]" },
    Subject = "Hello",
    Html = "<p>Hi</p>",
});
if (!check.CanSend)
{
    foreach (var issue in check.Issues)
        Console.WriteLine($"{issue.Field}: {issue.Error}");
}
 
// List recent emails, newest first. page is zero-based.
List<EmailRecord> emails = await axene.Emails.ListAsync(status: "delivered", page: 0, limit: 20);
 
// Fetch one email with its bodies and events.
EmailDetail detail = await axene.Emails.GetAsync(sent.Id);
 
// List delivery / open / click / bounce events for an email.
List<EmailEvent> events = await axene.Emails.EventsAsync(sent.Id);
 
// Re-send a bounced, rejected, or failed email as a new message.
SendEmailResult resent = await axene.Emails.RetryAsync(sent.Id);
 
// Search. q supports inline tokens: to:, from:, status:, domain:, tag:
List<EmailSearchHit> hits = await axene.Emails.SearchAsync(q: "welcome status:delivered", limit: 10);
 
// Poll for emails whose status changed at or after a timestamp (max 50).
List<EmailRecord> updated = await axene.Emails.UpdatesAsync(DateTimeOffset.UtcNow.AddMinutes(-1));

Scheduled emails:

List<ScheduledEmail> scheduled = await axene.Emails.ListScheduledAsync();
await axene.Emails.CancelScheduledAsync(scheduled[0].Id);
await axene.Emails.SendScheduledNowAsync(scheduled[0].Id);

Saved searches (named filter sets stored per user) are returned as raw JsonElement values:

var searches = await axene.Emails.GetSavedSearchesAsync();
await axene.Emails.SetSavedSearchesAsync(new object[]
{
    new { name = "Bounced today", query = "status:bounced" },
});

Domains

Accessed as client.Domains.

// List your sending domains and their verification status.
List<DomainRecord> domains = await axene.Domains.ListAsync();
 
// Register a new domain. Returns the DNS records to publish.
DomainDetail domain = await axene.Domains.CreateAsync("yourdomain.com");
 
// Fetch a domain with its DKIM selector and DNS records.
DomainDetail one = await axene.Domains.GetAsync(domain.Id);
 
// Re-check DNS and verify.
await axene.Domains.VerifyAsync(domain.Id);
 
// Live DNS health checks (DKIM, SPF, DMARC, return-path, MX).
DomainHealth health = await axene.Domains.HealthAsync(domain.Id);
 
// Diagnose configuration issues and get a health score.
DomainDiagnosis diagnosis = await axene.Domains.DiagnoseAsync(domain.Id);
 
// Rotate the DKIM key; returns the new record to publish.
DkimRotation rotation = await axene.Domains.RotateDkimAsync(domain.Id);
 
// Transfer the domain to another Axene account.
await axene.Domains.TransferAsync(domain.Id, "[email protected]", note: "handoff");
 
// Check availability against public DNS, or whether it already exists in your account.
DomainAvailability availability = await axene.Domains.CheckAvailabilityAsync("newdomain.com");
DomainCheck exists = await axene.Domains.CheckAsync("yourdomain.com");
 
// Delete a domain.
await axene.Domains.DeleteAsync(domain.Id);

Contacts

Subscriber lists, their contacts, CSV imports, and templated bulk sends. Accessed as client.Contacts.

// Lists.
List<ContactList> lists = await axene.Contacts.ListListsAsync();
ContactList list = await axene.Contacts.CreateListAsync("Newsletter", description: "Monthly news");
ContactListDetail listDetail = await axene.Contacts.GetListAsync(list.Id, page: 0, limit: 50);
await axene.Contacts.UpdateListAsync(list.Id, name: "Monthly Newsletter");
await axene.Contacts.DeleteListAsync(list.Id);
 
// Contacts within a list.
Contact contact = await axene.Contacts.AddContactAsync(list.Id, "[email protected]",
    name: "Subscriber", metadata: new { plan = "pro" });
await axene.Contacts.RemoveContactAsync(list.Id, contact.Id);

UploadCsvAsync takes the raw file bytes and a filename, sent as a single multipart field named file. The email column is auto-detected; other columns become contact metadata.

byte[] bytes = await File.ReadAllBytesAsync("contacts.csv");
CsvImportResult import = await axene.Contacts.UploadCsvAsync(list.Id, bytes, "contacts.csv");

BulkSendAsync mails a templated message to every contact in a list. Subject, html, and text may use {{email}}, {{name}}, and {{metadata_key}} placeholders:

BulkSendResult bulk = await axene.Contacts.BulkSendAsync(
    listId: list.Id,
    senderAddressId: "sender_123",
    subject: "Hello {{name}}",
    html: "<p>Hi {{name}}, here is the latest.</p>",
    tags: new[] { "newsletter" });

Suppressions

The do-not-send list. ListAsync returns a paginated Page<Suppression> envelope (Items, Total, Page, Limit). Accessed as client.Suppressions.

Page<Suppression> page = await axene.Suppressions.ListAsync(page: 0, limit: 50, search: "gmail.com");
Console.WriteLine($"{page.Items.Count} of {page.Total}");
 
await axene.Suppressions.AddAsync("[email protected]", reason: "manual");
 
// Bulk import from a file (one email per line) as a single multipart field.
byte[] bytes = await File.ReadAllBytesAsync("suppressions.txt");
BulkSuppressionResult result = await axene.Suppressions.BulkUploadAsync(bytes, "suppressions.txt");
 
await axene.Suppressions.RemoveAsync("suppression_id");

Templates

Reusable email templates (Starter plan and up). The SDK's html / text arguments map to the wire fields html_body / text_body for you. Accessed as client.Templates.

List<Template> templates = await axene.Templates.ListAsync();
 
Template template = await axene.Templates.CreateAsync(
    name: "Receipt",
    subject: "Your receipt",
    html: "<h1>Thanks, {{name}}</h1>",
    text: "Thanks, {{name}}");
 
Template fetched = await axene.Templates.GetAsync(template.Id);
await axene.Templates.UpdateAsync(template.Id, subject: "Your order receipt");
Template copy = await axene.Templates.DuplicateAsync(template.Id);
await axene.Templates.DeleteAsync(template.Id);

A template's variables are derived server-side from the {{name}} placeholders in its bodies, so you do not pass them when creating or updating.

Webhooks

Accessed as client.Webhooks.

List<Webhook> webhooks = await axene.Webhooks.ListAsync();
 
// Create one. The signing secret is generated and returned in plaintext.
Webhook webhook = await axene.Webhooks.CreateAsync(
    url: "https://example.com/hooks/axene",
    events: new[] { "email.delivered", "email.bounced" });
Console.WriteLine(webhook.Secret);
 
await axene.Webhooks.UpdateAsync(webhook.Id, events: new[] { "email.opened" }, isActive: true);
 
// Queue a sample email.delivered delivery to test the endpoint.
await axene.Webhooks.TestAsync(webhook.Id);
 
// Inspect delivery attempts (paginated envelope).
Page<WebhookDelivery> deliveries = await axene.Webhooks.ListDeliveriesAsync(webhook.Id, page: 0, limit: 20);
WebhookDeliveryDetail delivery = await axene.Webhooks.GetDeliveryAsync(webhook.Id, deliveries.Items[0].Id);
 
await axene.Webhooks.DeleteAsync(webhook.Id);

Error handling

Every non-2xx response, and any transport failure that survives all retries, throws an AxeneException. Inspect Status and Code to branch on specific failures.

using Axene.Mailer;
 
var axene = new AxeneMailerClient("axm_k_your_api_key");
 
try
{
    await axene.Emails.SendAsync(new SendEmail
    {
        From = "[email protected]",
        To = { "[email protected]" },
        Subject = "Hello",
        Html = "<p>Hi</p>",
    });
}
catch (AxeneException ex)
{
    Console.Error.WriteLine(ex.Status);  // HTTP status; 0 means a transport/network failure
    Console.Error.WriteLine(ex.Code);    // machine-readable code from the API body, when present
    Console.Error.WriteLine(ex.Message);
}

AxeneException.Status is 0 when no response was received (a transport or network failure that exhausted all retries). Code is populated from the API's detail body when present, and is null otherwise.

Configuration

  • Retries. Requests that return 429 or 5xx are retried with exponential backoff, honoring the Retry-After header when present. Control the total attempt count (including the first) with maxRetries (default 3). Multipart uploads (CSV and suppression imports) are not retried because they are not idempotent.
  • Custom HttpClient. Pass your own HttpClient, for example one from IHttpClientFactory, to share connection pooling and integrate with dependency injection. The SDK sets the Authorization header and base address on first use and does not dispose a client it did not create.
  • Custom base URL. Point the client at a staging or self-hosted host with the baseUrl parameter. A trailing slash is trimmed automatically.
var axene = new AxeneMailerClient(
    apiKey: Environment.GetEnvironmentVariable("AXENE_API_KEY")!,
    baseUrl: "https://staging-mail.axene.io",
    maxRetries: 5);

Pass a CancellationToken to any method to cancel an in-flight request:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await axene.Emails.SendAsync(message, cts.Token);

Next steps

On this page