Taifa MailTaifa Mail Docs
SDKs

Swift SDK

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

The official Swift SDK is published as the TaifaMail Swift package. Source lives in the taifa-mail-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/GovConnectKenya/taifa-mail-swift.git", from: "0.1.0")

Then add TaifaMail to your target's dependencies:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "TaifaMail", package: "taifa-mail-swift")
    ]
)

In Xcode, you can instead use File -> Add Packages, paste the repository URL https://github.com/GovConnectKenya/taifa-mail-swift.git, and add the TaifaMail library product to your target.

Client setup

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

import TaifaMail
 
let taifamail = TaifaMailClient(apiKey: ProcessInfo.processInfo.environment["TAIFA_MAIL_API_KEY"]!)

The convenience initializer accepts these parameters:

ParameterTypeDefaultDescription
apiKeyString(required)Your API key. Starts with tfm_k_.
baseURLStringhttps://govconnect.keOverride 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 taifamail = TaifaMailClient(
    apiKey: "tfm_k_...",
    baseURL: "https://govconnect.ke",
    maxRetries: 3,
    timeout: 30
)

The client exposes one handle per resource group: taifamail.emails, taifamail.domains, taifamail.contacts, taifamail.suppressions, taifamail.templates, and taifamail.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 TaifaMail
 
let taifamail = TaifaMailClient(apiKey: ProcessInfo.processInfo.environment["TAIFA_MAIL_API_KEY"]!)
 
let result = try await taifamail.emails.send(
    .init(
        from: Address(email: "hello@yourdomain.com", name: "Your Company"),
        to: ["customer@example.com"],
        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 "customer@example.com" is sugar for Address(email: "customer@example.com"). 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 taifamail.emails.send(
    .init(
        from: "hello@yourdomain.com",
        to: [Address(email: "a@example.com", name: "Ada"), "b@example.com"],
        subject: "Welcome aboard",
        html: "<p>Welcome!</p>",
        cc: ["manager@example.com"],
        replyTo: Address(email: "support@yourdomain.com"),
        tags: ["onboarding"],
        sendAt: "2026-06-14T09:00:00Z"
    )
)

Emails

// Send a single email.
let sent = try await taifamail.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 taifamail.emails.sendBatch([
    .init(from: from, to: ["a@example.com"], subject: subject, html: html),
    .init(from: from, to: ["b@example.com"], subject: subject, html: html)
])
print(batch.total, batch.sent, batch.failed, batch.results)
 
// Dry-run a send without sending it.
let check = try await taifamail.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 taifamail.emails.list(status: "delivered", page: 0, limit: 20)
 
// Fetch one email with bodies and events.
let detail = try await taifamail.emails.get(sent.id)
 
// List delivery / open / click / bounce events for an email.
let events = try await taifamail.emails.events(sent.id)
 
// Re-send a bounced, rejected, or failed email as a new message.
let resent = try await taifamail.emails.retry(sent.id)
 
// Search. q supports inline tokens: to:, from:, status:, domain:, tag:
let hits = try await taifamail.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 taifamail.emails.updates(since: "2026-06-13T10:00:00Z")

Scheduled emails:

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

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

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

Domains

// List your sending domains and their verification status.
let domains = try await taifamail.domains.list()
 
// Register a new domain. Returns the DNS records to publish.
let domain = try await taifamail.domains.create("yourdomain.com")
print(domain.dnsRecords)
 
// Fetch a domain with its DKIM selector and DNS records.
let one = try await taifamail.domains.get(domain.id)
 
// Re-check DNS and verify.
try await taifamail.domains.verify(domain.id)
 
// Live DNS health checks (DKIM, SPF, DMARC, return-path, MX).
let health = try await taifamail.domains.health(domain.id)
 
// Diagnose configuration issues and get a health score.
let diagnosis = try await taifamail.domains.diagnose(domain.id)
 
// Rotate the DKIM key; returns the new record to publish.
let rotation = try await taifamail.domains.rotateDkim(domain.id)
 
// Transfer the domain to another Taifa Mail account.
try await taifamail.domains.transfer(domain.id, targetEmail: "owner@other.com", note: "handoff")
 
// Check availability against public DNS, or whether it already exists in your account.
let availability = try await taifamail.domains.checkAvailability("newdomain.com")
let exists = try await taifamail.domains.check("yourdomain.com")
 
// Delete a domain.
try await taifamail.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 taifamail.contacts.listLists()
let list = try await taifamail.contacts.createList(name: "Newsletter", description: "Monthly news")
let listDetail = try await taifamail.contacts.getList(list.id, page: 0, limit: 50)
try await taifamail.contacts.updateList(list.id, name: "Monthly Newsletter")
try await taifamail.contacts.deleteList(list.id)
 
// Contacts within a list.
let contact = try await taifamail.contacts.addContact(
    list.id,
    email: "subscriber@example.com",
    name: "Subscriber",
    metadata: ["plan": .string("pro")]
)
try await taifamail.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 taifamail.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 taifamail.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 taifamail.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 taifamail.suppressions.add(email: "unsubscribed@example.com", 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 taifamail.suppressions.bulkUpload(file: bytes, filename: "suppressions.txt")
print(bulk.added, bulk.skipped, bulk.totalProcessed)
 
try await taifamail.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 taifamail.templates.list()
 
let template = try await taifamail.templates.create(
    name: "Receipt",
    subject: "Your receipt",
    html: "<h1>Thanks, {{name}}</h1>",
    text: "Thanks, {{name}}"
)
 
let fetched = try await taifamail.templates.get(template.id)
try await taifamail.templates.update(template.id, subject: "Your order receipt")
let copy = try await taifamail.templates.duplicate(template.id)
try await taifamail.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 taifamail.webhooks.list()
 
// Create one. The signing secret is generated and returned in plaintext.
let webhook = try await taifamail.webhooks.create(
    url: "https://example.com/hooks/taifamail",
    events: ["email.delivered", "email.bounced"]
)
print(webhook.secret)
 
// The clean `isActive` maps to the wire field `is_active`.
try await taifamail.webhooks.update(webhook.id, events: ["email.opened"], isActive: true)
 
// Queue a sample email.delivered delivery to test the endpoint.
try await taifamail.webhooks.test(webhook.id)
 
// Inspect delivery attempts (paginated envelope).
let deliveries = try await taifamail.webhooks.listDeliveries(webhook.id, page: 0, limit: 20)
let delivery = try await taifamail.webhooks.getDelivery(webhook.id, deliveryId: deliveries.items[0].id)
print(delivery.payload, delivery.responseBody as Any)
 
try await taifamail.webhooks.delete(webhook.id)

Error handling

Every non-2xx response, and any transport failure that survives all retries, throws an TaifaMailError. 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 TaifaMail
 
do {
    try await taifamail.emails.send(
        .init(
            from: "hello@yourdomain.com",
            to: ["customer@example.com"],
            subject: "Hello",
            html: "<p>Hi</p>"
        )
    )
} catch let error as TaifaMailError {
    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
}

TaifaMailError 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 taifamail = TaifaMailClient(
    apiKey: "tfm_k_...",
    baseURL: "https://staging.govconnect.ke",
    maxRetries: 5,
    timeout: 10
)

Next steps

On this page