Contents

brightdigit/spinetail

A lightweight async Swift client for the Mailchimp Marketing API,

What's a _Spinetail_?

A Spinetail is a type of Swift bird which shares its habitat with chimps (such as the chimp in Mailchimp).

🎁 Features

  • βœ… Async/await API β€” no completion handlers, no Combine.
  • βœ… Datacenter-aware β€” the API host is derived automatically from your API

key's -<datacenter> suffix (e.g. ...-us21 β†’ https://us21.api.mailchimp.com/3.0).

  • βœ… HTTP Basic auth built in β€” pass your API key, nothing else.
  • βœ… Testable by design β€” inject any

ClientTransport to replay fixtures instead of hitting the network.

  • βœ… Broad platform support β€” Linux, macOS, iOS, tvOS, watchOS, Windows,

Android, and WebAssembly (Wasm).

πŸ— Installation

Add Spinetail as a dependency in your Package.swift:

dependencies: [
  .package(url: "https://github.com/brightdigit/Spinetail.git", from: "1.0.0-alpha.1")
]

Then add it to your target:

.target(
  name: "MyTarget",
  dependencies: [
    .product(name: "Spinetail", package: "Spinetail")
  ]
)

Spinetail requires Swift 6.4 and supports macOS 13+, iOS 16+, tvOS 16+, and watchOS 9+ (plus Linux, Windows, Android, and Wasm).

πŸ’ͺ Usage

Creating a client

Your Mailchimp API key ends in a -<datacenter> suffix; Spinetail uses it to derive the correct API host, so you only need to supply the key:

import Spinetail

let client = try MailchimpClient(
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us21"
)

This convenience initializer uses the default URLSessionTransport. On platforms without URLSession (Wasm), or when you want to inject a custom transport, pass one explicitly:

let client = try MailchimpClient(
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us21",
  transport: myTransport
)

Listing sent campaigns

sentCampaigns(forListID:) returns the sent campaigns for a given Mailchimp list (audience), most recent first, mapped into the flat `Campaign` model:

let campaigns = try await client.sentCampaigns(forListID: "your-list-id")

for campaign in campaigns {
  print(campaign.title ?? "Untitled", campaign.sendTime ?? .distantPast)
}

Each Campaign exposes the fields the importer reads β€” all optional by design (the consumer validates presence):

| Property | Source | | -------------------- | ----------------------------- | | id | id | | longArchiveURL | long_archive_url | | sendTime | send_time | | subjectLine | settings.subject_line | | title | settings.title | | previewText | settings.preview_text | | segmentText | recipients.segment_text | | socialCardImageURL | social_card.image_url |

Fetching a campaign's archive HTML

archiveHTML(forCampaignID:) returns the rendered archive HTML for a campaign:

let html = try await client.archiveHTML(forCampaignID: "campaign-id")

campaignContent(forCampaignID:) and plainText(forCampaignID:) are convenience methods over the same generated getCampaignsIdContent operation β€” campaignContent returns both archive_html and plain_text in one request, and plainText unwraps just the plain-text body.

let content = try await client.campaignContent(forCampaignID: "campaign-id")
let plain = try await client.plainText(forCampaignID: "campaign-id")

Error handling

Invalid HTTP responses and missing content fields are mapped to MailchimpClient.ClientError. Errors thrown by the generated client or transport (for example network failures) propagate unchanged.

| Case | When | | ----------------------------- | ----------------------------------------------------------- | | .invalidAPIKey | The API key has no -<datacenter> suffix to derive a host. | | .invalidResponse | The server returned a non-200 / undocumented response. | | .missingHTML(campaignID:) | The campaign returned no archive_html. | | .missingPlainText(campaignID:) | The campaign returned no plain_text. |

do {
  let html = try await client.archiveHTML(forCampaignID: id)
} catch MailchimpClient.ClientError.missingHTML(let campaignID) {
  print("No archive HTML for \(campaignID)")
}

πŸ› Architecture

Spinetail is two layered targets:

  • SpinetailOpenAPI β€” generated, committed code (Types.swift,

Client.swift). Produced ahead of time (not by a build plugin) by Scripts/generate-openapi-spinetail.sh, which filters the full Mailchimp spec down to just getCampaigns and getCampaignsIdContent. Never edit this by hand β€” change the spec/config and regenerate.

  • Spinetail β€” the hand-written public layer and the only target consumers

import: - MailchimpClient β€” the public client (host derivation + the two operations). - AuthenticationMiddleware β€” adds HTTP Basic auth (API key as the password; username ignored by Mailchimp). - Campaign β€” the flat DTO mapped from the generated campaign payload.

OpenAPIURLSession (the URLSession transport) does not build for WebAssembly, so anything depending on it is gated behind #if !os(WASI); Wasm callers supply an explicit transport.

πŸ“ž Supported Requests

Spinetail deliberately exposes only what its consumer needs:

| Operation | Mailchimp endpoint | MailchimpClient method | | ------------------------ | ----------------------------- | ------------------------------- | | getCampaigns | GET /campaigns | sentCampaigns(forListID:) | | getCampaignsIdContent | GET /campaigns/{id}/content | archiveHTML(forCampaignID:) |

To support more, see Adding an API operation.

πŸ‘· Development

Tooling is pinned in .mise.toml and run through mise exec --. Install once with mise install.

  • Build: swift build
  • Test: swift test (the suite uses

swift-testing, not XCTest)

  • Lint + format + build check: ./Scripts/lint.sh
  • Regenerate the OpenAPI client: ./Scripts/generate-openapi-spinetail.sh

Adding an API operation

  1. Add the operation id to filter: operations: in

Sources/SpinetailOpenAPI/openapi-generator-config.yaml.

  1. Regenerate with ./Scripts/generate-openapi-spinetail.sh (regeneration must

be diff-free unless the spec, config, or generator version changed).

  1. Surface the new operation through MailchimpClient.

πŸ™ Acknowledgments

Thanks to Apple's swift-openapi-generator team, and to Yonas Kolb whose SwagGen powered earlier versions of this package.

πŸ“œ License

This code is distributed under the MIT license. See the LICENSE file for more info.

Package Metadata

Repository: brightdigit/spinetail

Default branch: main

README: README.md