A lightweight async Swift client for the Mailchimp Marketing API, built on swift-openapi-generator.
- π¬ Introduction
- π Features
- π Installation
- πͺ Usage
- π Architecture
- π Supported Requests
- π· Development
- π Acknowledgments
- π License
Spinetail is a narrow, purpose-built async client for the Mailchimp
Marketing API. It is intentionally small: it wraps two generated operations β
listing sent campaigns and fetching a campaign's content β and exposes
a thin public surface for its consumer
(ContributeMailchimp, a newsletter
importer): listing sent campaigns, fetching archive HTML, plus convenience
helpers for the full content payload and plain text.
Under the hood it wraps a client generated by
swift-openapi-generator from
Mailchimp's OpenAPI spec,
filtered down to just those two operations. The hand-written Spinetail layer
adds datacenter-host derivation, HTTP Basic authentication, and a flat campaign
DTO.
Looking for the old SwagGen/Prch API? Earlier versions of Spinetail were built on Prch/SwagGen and exposed
Mailchimp.API,Client(api:session:), and request/response objects. That architecture was replaced by the swift-openapi-generator migration. The current public surface isMailchimpClient, documented below.
A Spinetail is a type of Swift bird which shares its habitat with chimps (such as the chimp in Mailchimp).
- β 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
ClientTransportto replay fixtures instead of hitting the network. - β Broad platform support β Linux, macOS, iOS, tvOS, watchOS, Windows, Android, and WebAssembly (Wasm).
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).
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
)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 |
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")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)")
}Spinetail is two layered targets:
SpinetailOpenAPIβ generated, committed code (Types.swift,Client.swift). Produced ahead of time (not by a build plugin) byScripts/generate-openapi-spinetail.sh, which filters the full Mailchimp spec down to justgetCampaignsandgetCampaignsIdContent. 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.
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.
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
- Add the operation id to
filter: operations:inSources/SpinetailOpenAPI/openapi-generator-config.yaml. - Regenerate with
./Scripts/generate-openapi-spinetail.sh(regeneration must be diff-free unless the spec, config, or generator version changed). - Surface the new operation through
MailchimpClient.
Thanks to Apple's swift-openapi-generator team, and to Yonas Kolb whose SwagGen powered earlier versions of this package.
This code is distributed under the MIT license. See the LICENSE file for more info.