AxeneAxene Docs
SDKs

Swift SDK

Send email, manage domains, contacts, templates, and webhooks from Swift on macOS and iOS with the official AxeneMailer package.

The official Swift SDK is published as the AxeneMailer Swift package. Source lives in the axene-mailer-swift repository.

The package is built on async/await and Foundation's URLSession, with no third-party dependencies. It supports macOS 12+ and iOS 15+.

Installation

Add the package to the dependencies in your Package.swift:

.package(url: "https://github.com/Axene-Solutions/axene-mailer-swift.git", from: "0.1.0")

Then add AxeneMailer to your target's dependencies:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "AxeneMailer", package: "axene-mailer-swift")
    ]
)

In Xcode, you can instead use File -> Add Packages, paste the repository URL https://github.com/Axene-Solutions/axene-mailer-swift.git, and add the AxeneMailer library product to your target.

Client setup

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

import AxeneMailer
 
let axene = AxeneClient(apiKey: ProcessInfo.processInfo.environment["AXENE_API_KEY"]!)

The convenience initializer accepts these parameters:

ParameterTypeDefaultDescription
apiKeyString(required)Your API key. Starts with axm_k_.
baseURLStringhttps://mail.axene.ioOverride the API base URL.
maxRetriesInt3Total attempts on 429 / 5xx, including the first.
timeoutTimeInterval30Per-request timeout in seconds.
sessionURLSession.sharedInject a custom URLSession (for testing).
let axene = AxeneClient(
    apiKey: "axm_k_...",
    baseURL: "https://mail.axene.io",
    maxRetries: 3,
    timeout: 30
)

The client exposes one handle per resource group: axene.emails, axene.domains, axene.contacts, axene.suppressions, axene.templates, and axene.webhooks. Every method is async throws.

Send an email

emails.send(_:) queues a single message and returns its id and initial status. Address fields accept a bare string literal, which the SDK treats as an Address(email:).

import AxeneMailer
 
let axene = AxeneClient(apiKey: ProcessInfo.processInfo.environment["AXENE_API_KEY"]!)
 
let result = try await axene.emails.send(
    .init(
        from: Address(email: "[email protected]", name: "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."
    )
)
 
print(result.id, result.status)

The response is a SendEmailResponse with id, status, messageId, and rejectionReason.

A few conveniences worth knowing:

  • The SDK exposes a clean from field, then maps it to the wire field from_ for you. You never write from_.
  • Address conforms to ExpressibleByStringLiteral, so "[email protected]" is sugar for Address(email: "[email protected]"). Use Address(email:name:) when you want a display name.
  • Provide html, text, or both.
  • sendAt accepts an ISO 8601 string and schedules the message for later (Starter plan and up).
try await axene.emails.send(
    .init(
        from: "[email protected]",
        to: [Address(email: "[email protected]", name: "Ada"), "[email protected]"],
        subject: "Welcome aboard",
        html: "<p>Welcome!</p>",
        cc: ["[email protected]"],
        replyTo: Address(email: "[email protected]"),
        tags: ["onboarding"],
        sendAt: "2026-06-14T09:00:00Z"
    )
)

Emails

// Send a single email.
let sent = try await axene.emails.send(.init(from: from, to: to, subject: subject, html: html))
 
// Send a batch (bare array; Starter plan and up, capped by your plan).
let batch = try await axene.emails.sendBatch([
    .init(from: from, to: ["[email protected]"], subject: subject, html: html),
    .init(from: from, to: ["[email protected]"], subject: subject, html: html)
])
print(batch.total, batch.sent, batch.failed, batch.results)
 
// Dry-run a send without sending it.
let check = try await axene.emails.validate(.init(from: from, to: to, subject: subject, html: html))
if !check.canSend { print(check.issues) }
 
// List recent emails (newest first). page is zero-based.
let emails = try await axene.emails.list(status: "delivered", page: 0, limit: 20)
 
// Fetch one email with bodies and events.
let detail = try await axene.emails.get(sent.id)
 
// List delivery / open / click / bounce events for an email.
let events = try await axene.emails.events(sent.id)
 
// Re-send a bounced, rejected, or failed email as a new message.
let resent = try await axene.emails.retry(sent.id)
 
// Search. q supports inline tokens: to:, from:, status:, domain:, tag:
let hits = try await axene.emails.search(q: "welcome status:delivered", limit: 10)
 
// Poll for emails whose status changed at or after a timestamp (max 50).
let updated = try await axene.emails.updates(since: "2026-06-13T10:00:00Z")

Scheduled emails:

let scheduled = try await axene.emails.listScheduled()
try await axene.emails.cancelScheduled(scheduled[0].id)
try await axene.emails.sendScheduledNow(scheduled[0].id)

Saved searches (named filter sets stored per user) are loosely typed as JSONObject:

let searches = try await axene.emails.getSavedSearches()
try await axene.emails.setSavedSearches([
    ["name": .string("Bounced today"), "query": .string("status:bounced")]
])

Domains

// List your sending domains and their verification status.
let domains = try await axene.domains.list()
 
// Register a new domain. Returns the DNS records to publish.
let domain = try await axene.domains.create("yourdomain.com")
print(domain.dnsRecords)
 
// Fetch a domain with its DKIM selector and DNS records.
let one = try await axene.domains.get(domain.id)
 
// Re-check DNS and verify.
try await axene.domains.verify(domain.id)
 
// Live DNS health checks (DKIM, SPF, DMARC, return-path, MX).
let health = try await axene.domains.health(domain.id)
 
// Diagnose configuration issues and get a health score.
let diagnosis = try await axene.domains.diagnose(domain.id)
 
// Rotate the DKIM key; returns the new record to publish.
let rotation = try await axene.domains.rotateDkim(domain.id)
 
// Transfer the domain to another Axene account.
try await axene.domains.transfer(domain.id, targetEmail: "[email protected]", note: "handoff")
 
// Check availability against public DNS, or whether it already exists in your account.
let availability = try await axene.domains.checkAvailability("newdomain.com")
let exists = try await axene.domains.check("yourdomain.com")
 
// Delete a domain.
try await axene.domains.delete(domain.id)

The niche domain endpoints (DNS provider hints, BIMI, and Domain Connect) are not covered by this version of the SDK. Call them directly over HTTP if you need them.

Contacts

Subscriber lists, their contacts, CSV imports, and templated bulk sends.

// Lists.
let lists = try await axene.contacts.listLists()
let list = try await axene.contacts.createList(name: "Newsletter", description: "Monthly news")
let listDetail = try await axene.contacts.getList(list.id, page: 0, limit: 50)
try await axene.contacts.updateList(list.id, name: "Monthly Newsletter")
try await axene.contacts.deleteList(list.id)
 
// Contacts within a list.
let contact = try await axene.contacts.addContact(
    list.id,
    email: "[email protected]",
    name: "Subscriber",
    metadata: ["plan": .string("pro")]
)
try await axene.contacts.removeContact(list.id, contactId: contact.id)

uploadCsv takes the raw file bytes (Data) and a filename, sent as a single multipart field named file:

let bytes = try Data(contentsOf: URL(fileURLWithPath: "contacts.csv"))
let importResult = try await axene.contacts.uploadCsv(list.id, file: bytes, filename: "contacts.csv")
print(importResult.imported, importResult.skipped)

bulkSend mails a templated message to every contact in a list. Subject, html, and text may use {{email}}, {{name}}, and {{metadata_key}} placeholders. The contact_list_id field is injected for you to match the list id:

let result = try await axene.contacts.bulkSend(
    list.id,
    senderAddressId: "sender_123",
    subject: "Hello {{name}}",
    html: "<p>Hi {{name}}, here is the latest.</p>",
    tags: ["newsletter"]
)
print(result.queued, result.skipped)

Suppressions

The do-not-send list. list returns a paginated Page envelope (items, total, page, limit).

let page = try await axene.suppressions.list(page: 0, limit: 50, search: "gmail.com")
print(page.items, page.total)
 
// The clean `email` maps to the wire field `email_address`.
try await axene.suppressions.add(email: "[email protected]", reason: "manual")
 
// Bulk import from a file (one email per line) as a single multipart field.
let bytes = try Data(contentsOf: URL(fileURLWithPath: "suppressions.txt"))
let bulk = try await axene.suppressions.bulkUpload(file: bytes, filename: "suppressions.txt")
print(bulk.added, bulk.skipped, bulk.totalProcessed)
 
try await axene.suppressions.remove("suppression_id")

Templates

Reusable email templates. html maps to the wire field html_body and text to text_body for you.

let templates = try await axene.templates.list()
 
let template = try await axene.templates.create(
    name: "Receipt",
    subject: "Your receipt",
    html: "<h1>Thanks, {{name}}</h1>",
    text: "Thanks, {{name}}"
)
 
let fetched = try await axene.templates.get(template.id)
try await axene.templates.update(template.id, subject: "Your order receipt")
let copy = try await axene.templates.duplicate(template.id)
try await axene.templates.delete(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. Templates require the Starter plan or above.

Webhooks

let webhooks = try await axene.webhooks.list()
 
// Create one. The signing secret is generated and returned in plaintext.
let webhook = try await axene.webhooks.create(
    url: "https://example.com/hooks/axene",
    events: ["email.delivered", "email.bounced"]
)
print(webhook.secret)
 
// The clean `isActive` maps to the wire field `is_active`.
try await axene.webhooks.update(webhook.id, events: ["email.opened"], isActive: true)
 
// Queue a sample email.delivered delivery to test the endpoint.
try await axene.webhooks.test(webhook.id)
 
// Inspect delivery attempts (paginated envelope).
let deliveries = try await axene.webhooks.listDeliveries(webhook.id, page: 0, limit: 20)
let delivery = try await axene.webhooks.getDelivery(webhook.id, deliveryId: deliveries.items[0].id)
print(delivery.payload, delivery.responseBody as Any)
 
try await axene.webhooks.delete(webhook.id)

Error handling

Every non-2xx response, and any transport failure that survives all retries, throws an AxeneError. Inspect status and code to branch on specific failures. A status of 0 means a transport or network failure where no HTTP response was received.

import AxeneMailer
 
do {
    try await axene.emails.send(
        .init(
            from: "[email protected]",
            to: ["[email protected]"],
            subject: "Hello",
            html: "<p>Hi</p>"
        )
    )
} catch let error as AxeneError {
    print(error.status)  // HTTP status; 0 means a transport/network failure
    print(error.code as Any)  // machine-readable code from the API body, when present
    print(error.message)
} catch {
    throw error
}

AxeneError conforms to LocalizedError and CustomStringConvertible, so its errorDescription is the message and printing it gives a status / code / message summary.

Configuration

  • Retries. Requests that return 429 or 5xx are retried with exponential backoff, honouring the Retry-After header when present. Control the total attempt count (including the first) with maxRetries (default 3). Multipart uploads are not retried because they are not idempotent.
  • Timeout. Each request times out after timeout seconds (default 30). A timeout is treated as a transport failure and is retried while attempts remain.
  • Custom base URL. Point the client at a staging or self-hosted host with baseURL. A trailing slash is trimmed automatically.
  • Custom session. Pass a URLSession to share configuration or to inject a mock transport in tests.
let axene = AxeneClient(
    apiKey: "axm_k_...",
    baseURL: "https://staging-mail.axene.io",
    maxRetries: 5,
    timeout: 10
)

Next steps

On this page