Spinetail uses URLSession for network communication via Prch.
However if you are building a server-side application in Swift and wish to take advantage of SwiftNIO, then you’ll want import PrchNIO package as well:
Now that we have setup the client, we’ll be using let’s begin to access the Mailchimp API.
💪 Usage
🕊 Prch Basics
To make a request via Prch, we have three options using our client:
closure-based completion calls
async/await
synchronous calls
Closure-based Completion
client.request(request) { result in
switch result {
case let .success(member):
// Successful Retrieval
break
case let .defaultResponse(statusCode, response):
// Non-2xx Response (ex. 404 member not found)
break
case let .failure(error):
// Other Errors (ex. networking, decoding or encoding JSON...)
break
}
}
Async/Await
do {
// Successful Retrieval
let member = try await client.request(request)
} catch let error as ClientResponseResult<Lists.GetListsIdMembersId.Response>.FailedResponseError {
// Non-2xx Response (ex. 404 member not found)
} catch {
// Other Errors (ex. networking, decoding or encoding JSON...)
}
Synchronous
do {
// Successful Retrieval
let member = try client.requestSync(request)
} catch let error as ClientResponseResult<Lists.GetListsIdMembersId.Response>.FailedResponseError {
// Non-2xx Response (ex. 404 member not found)
} catch {
// Other Errors (ex. networking, decoding or encoding JSON...)
}
In each case there are possible results:
The call was successful
The call failed but the response was valid such as a 4xx status code
The call failed due to an internal error (ex. decoding, encoding, networking, etc…)
Let’s start with an example using audience member lists.
The MD5 hash of the lowercase version of the list member’s email address. This endpoint also accepts a list member’s email address or contact_id.
The means we can use:
MD5 hash of the lowercase version of the list member’s email address but also
email address or
contact_id
In our case, we’ll be using an email address to see if we have someone subscribed.
Additionally we need our audience’s listID which is found on the audience settings page.
With that email address, we can create a Request:
import Spinetail
let api = Mailchimp.API(apiKey: "")
let client = Client(api: api, session: URLSession.shared)
let request = Lists.GetListsIdMembersId.Request(listId: listId, subscriberHash: emailAddress)
As previously noted there are three ways to execute a call. In this case, let’s use the synchronous call:
do {
// Successful Retrieval
let member = try client.requestSync(request)
} catch let error as ClientResponseResult<Lists.GetListsIdMembersId.Response>.FailedResponseError {
// Non-2xx Response (ex. 404 member not found)
} catch {
// Other Errors (ex. networking, decoding or encoding JSON...)
}
This is a good example of where we’d want to handle a 404. If the member is found, we may need to just update them, otherwise we want go ahead and add that subscriber.
Now that we have a request let’s use the completion handler call for adding a new member:
client.request(request) { result in
switch result {
case let .success(newMember):
// Successful Adding
break
case let .defaultResponse(statusCode, response):
// Non-2xx Response
break
case let .failure(error):
// Other Errors (ex. networking, decoding or encoding JSON...)
break
}
}
import Fluent
import Prch
import PrchVapor
import Spinetail
import Vapor
struct MailchimpMiddleware: ModelMiddleware {
// our client created during server initialization
let client: Prch.Client<PrchVapor.SessionClient, Spinetail.Mailchimp.API>
// the list id
let listID: String
// the interest id
let interestID : String
func upsertSubscriptionForUser(
_ user: User,
withEventLoop eventLoop: EventLoop
) -> EventLoopFuture<Void> {
let memberRequest = Lists.GetListsIdMembersId.Request(listId: listID, subscriberHash: user.email)
// find the subscription member
return client.request(memberRequest).flatMapThrowing { response -> in
switch response {
case .defaultResponse(statusCode: 404, _):
return nil
case let .success(member):
return member
default:
throw ClientError.invalidResponse
}
}.flatMap { member in
// if the subscriber already exists and has the interest id, don't do anything
if member?.interests?[self.interestID] == true {
return eventLoop.future()
// if the subscriber already exists but doesn't have the interest id
} else if let subscriberHash = member?.id {
// update the subscriber
let patch = Lists.PatchListsIdMembersId.Request(body:
.init(
emailAddress: user.email,
emailType: nil,
interests: [self.interestID: true]),
options: Lists.PatchListsIdMembersId.Request.Options(
listId: self.listID,
subscriberHash: subscriberHash
)
)
// transform to `Void` on success
return client.request(patch).success()
// if the subscriber doesn't already exists
} else {
// update the subscriber add them
let post = Lists.PostListsIdMembers.Request(
listId: self.listID,
body: .init(
emailAddress: user.email,
status: .subscribed,
interests: [self.interestID: true],
timestampOpt: .init(),
timestampSignup: .init()
)
)
// transform to `Void` on success
return client.request(post).success()
}
}
}
// after adding the row to the db, add the user to our subscription list with the interest id
func create(model: User, on db: Database, next: AnyModelResponder) -> EventLoopFuture<Void> {
next.create(model, on: db).transform(to: model).flatMap { user in
self.upsertSubscriptionForUser(user, withEventLoop: db.eventLoop)
}
}
}
Now that we have an example dealing with managing members, let’s look at how to get a list of campaigns and email our subscribers in Swift.
📩 Templates and Campaigns
With newsletters there are campaigns and templates.
Campaigns are how you send emails to your Mailchimp list. A template is an HTML file used to create the layout and basic design for a campaign.
Before creating our own campaign and template, let’s look at how to pull a list of campaigns.
Pulling List of Campaigns
On the BrightDigit web site, I want to link to each newsletter that’s sent out. To do this you just need the listID again.
We’ll be pulling up to 1000 sent campaigns sorted from last sent to first sent:
let request = Campaigns.GetCampaigns.Request(
count: 1000,
status: .sent,
listId: listID,
sortField: .sendTime,
sortDir: .desc
)
let response = try self.requestSync(request)
let campaigns = response.campaigns ?? []
To get the content we to grab it based on each campaign’s campaignID.
Get Newsletter Content
Before grabbing the content, we need to grab the campaignID from the campaign:
let campaign : Campaigns.GetCampaigns.Response.Status200.Campaigns
let html: String
guard let campaignID = campaign.id else {
return
}
html = try self.htmlFromCampaign(withID: campaignProperties.campaignID)
Creating a Template
To actually send we need to create an template using the
POST request. Here’s an example with async and await:
let templateName = "Example Email"
let templateHTML = "<strong>Hello World</strong>"
let templateRequest = Templates.PostTemplates.Request(body: .init(html: templateHTML, name: templateName))
let template = try await client.request(templateRequest)
Let’s use the template to create a campaign and send it.
Send an Campaign Email to Our Audience List
// make sure to get the templateID
guard let templateID = template.id else {
return
}
// set the email settings
let settings: Campaigns.PostCampaigns.Request.Body.Settings = .init(
fromName: "Leo",
replyTo: "leo@brightdigit.com",
subjectLine: "Hello World - Test Email",
templateId: templateID
)
// set the type and list you're sending to
let body: Campaigns.PostCampaigns.Request.Body = .init(
type: .regular,
contentType: .template,
recipients: .init(listId: listID),
settings: settings
)
let request = Campaigns.PostCampaigns.Request(body: body)
await client.request(request)
Due to the limitation of existing 32-bit watchOS devices, the library need to exclude certain APIs to limit size.
Therefore these sets of APIs are available on all operating systems and platforms including watchOS.
Campaigns
Request
Tested
Documented
watchOS
DeleteCampaignsId
✅
DeleteCampaignsIdFeedbackId
✅
GetCampaigns
✅
GetCampaignsId
✅
GetCampaignsIdContent
✅
✅
GetCampaignsIdFeedback
✅
GetCampaignsIdFeedbackId
✅
GetCampaignsIdSendChecklist
✅
PatchCampaignsId
✅
PatchCampaignsIdFeedbackId
✅
PostCampaigns
✅
✅
✅
PostCampaignsIdActionsCancelSend
✅
PostCampaignsIdActionsCreateResend
✅
PostCampaignsIdActionsPause
✅
PostCampaignsIdActionsReplicate
✅
PostCampaignsIdActionsResume
✅
PostCampaignsIdActionsSchedule
✅
PostCampaignsIdActionsSend
✅
PostCampaignsIdActionsTest
✅
PostCampaignsIdActionsUnschedule
✅
PostCampaignsIdFeedback
✅
PutCampaignsIdContent
✅
Lists
Request
Tested
Documented
watchOS
DeleteListsId
✅
DeleteListsIdInterestCategoriesId
✅
DeleteListsIdInterestCategoriesIdInterestsId
✅
DeleteListsIdMembersId
✅
DeleteListsIdMembersIdNotesId
✅
DeleteListsIdMergeFieldsId
✅
DeleteListsIdSegmentsId
✅
DeleteListsIdSegmentsIdMembersId
✅
DeleteListsIdWebhooksId
✅
GetListMemberTags
✅
GetLists
✅
GetListsId
✅
GetListsIdAbuseReports
✅
GetListsIdAbuseReportsId
✅
GetListsIdActivity
✅
GetListsIdClients
✅
GetListsIdGrowthHistory
✅
GetListsIdGrowthHistoryId
✅
GetListsIdInterestCategories
✅
GetListsIdInterestCategoriesId
✅
GetListsIdInterestCategoriesIdInterests
✅
GetListsIdInterestCategoriesIdInterestsId
✅
GetListsIdLocations
✅
GetListsIdMembers
✅
GetListsIdMembersId
✅
✅
✅
GetListsIdMembersIdActivity
✅
GetListsIdMembersIdActivityFeed
✅
GetListsIdMembersIdEvents
✅
GetListsIdMembersIdGoals
✅
GetListsIdMembersIdNotes
✅
GetListsIdMembersIdNotesId
✅
GetListsIdMergeFields
✅
GetListsIdMergeFieldsId
✅
GetListsIdSegmentsId
✅
GetListsIdSegmentsIdMembers
✅
GetListsIdSignupForms
✅
GetListsIdWebhooks
✅
GetListsIdWebhooksId
✅
PatchListsId
✅
PatchListsIdInterestCategoriesId
✅
PatchListsIdInterestCategoriesIdInterestsId
✅
PatchListsIdMembersId
✅
✅
✅
PatchListsIdMembersIdNotesId
✅
PatchListsIdMergeFieldsId
✅
PatchListsIdSegmentsId
✅
PatchListsIdWebhooksId
✅
PostListMemberEvents
✅
PostListMemberTags
✅
PostLists
✅
PostListsId
✅
PostListsIdInterestCategories
✅
PostListsIdInterestCategoriesIdInterests
✅
PostListsIdMembers
✅
✅
✅
PostListsIdMembersHashActionsDeletePermanent
✅
PostListsIdMembersIdNotes
✅
PostListsIdMergeFields
✅
PostListsIdSegments
✅
PostListsIdSegmentsId
✅
PostListsIdSegmentsIdMembers
✅
PostListsIdSignupForms
✅
PostListsIdWebhooks
✅
PreviewASegment
✅
PutListsIdMembersId
✅
SearchTagsByName
✅
Templates
Request
Tested
Documented
watchOS
GetTemplates
✅
GetTemplatesId
✅
GetTemplatesIdDefaultContent
✅
PatchTemplatesId
✅
PostTemplates
✅
✅
✅
Testing Pending
Request
Tested
Documented
watchOS
DeleteCampaignFoldersId
✅
GetCampaignFolders
✅
GetCampaignFoldersId
✅
PatchCampaignFoldersId
✅
PostCampaignFolders
✅
Template Folders
Request
Tested
Documented
watchOS
DeleteTemplateFoldersId
✅
GetTemplateFolders
✅
GetTemplateFoldersId
✅
PatchTemplateFoldersId
✅
PostTemplateFolders
✅
DeleteTemplatesId
✅
Search Campaigns
Request
Tested
Documented
watchOS
GetSearchCampaigns
✅
Search Members
Request
Tested
Documented
watchOS
GetSearchMembers
✅
Reports
Request
Tested
Documented
watchOS
GetReports
✅
GetReportsId
✅
GetReportsIdAbuseReportsId
✅
GetReportsIdAbuseReportsIdId
✅
GetReportsIdAdvice
✅
GetReportsIdClickDetails
✅
GetReportsIdClickDetailsId
✅
GetReportsIdClickDetailsIdMembers
✅
GetReportsIdClickDetailsIdMembersId
✅
GetReportsIdDomainPerformance
✅
GetReportsIdEcommerceProductActivity
✅
GetReportsIdEepurl
✅
GetReportsIdEmailActivity
✅
GetReportsIdEmailActivityId
✅
GetReportsIdLocations
✅
GetReportsIdOpenDetails
✅
GetReportsIdOpenDetailsIdMembersId
✅
GetReportsIdSentTo
✅
GetReportsIdSentToId
✅
GetReportsIdSubReportsId
✅
GetReportsIdUnsubscribed
✅
GetReportsIdUnsubscribedId
✅
Root
Request
Tested
Documented
watchOS
GetRoot
✅
😊 Pending Next Support
These are the next set of API for which migrating to watchOS is desired as well as more robust testing and documentation.
If you have any requests feel free to submit an issue or pull-request to improve current support.
File Manager
Request
Tested
Documented
watchOS
DeleteFileManagerFilesId
DeleteFileManagerFoldersId
GetFileManagerFiles
GetFileManagerFilesId
GetFileManagerFolders
GetFileManagerFoldersId
PatchFileManagerFilesId
PatchFileManagerFoldersId
PostFileManagerFiles
PostFileManagerFolders
Batches
Request
Tested
Documented
watchOS
DeleteBatchesId
GetBatches
GetBatchesId
PostBatches
DeleteBatchWebhookId
GetBatchWebhook
GetBatchWebhooks
PatchBatchWebhooks
PostBatchWebhooks
Automations
Request
Tested
Documented
watchOS
ArchiveAutomations
DeleteAutomationsIdEmailsId
GetAutomations
GetAutomationsId
GetAutomationsIdEmails
GetAutomationsIdEmailsId
GetAutomationsIdEmailsIdQueue
GetAutomationsIdEmailsIdQueueId
GetAutomationsIdRemovedSubscribers
GetAutomationsIdRemovedSubscribersId
PatchAutomationEmailWorkflowId
PostAutomations
PostAutomationsIdActionsPauseAllEmails
PostAutomationsIdActionsStartAllEmails
PostAutomationsIdEmailsIdActionsPause
PostAutomationsIdEmailsIdActionsStart
PostAutomationsIdEmailsIdQueue
PostAutomationsIdRemovedSubscribers
😌 Remaining Requests
These are the least priority set of API for which migrating to watchOS as well as robust testing and documentation have been prioritized.
If you have any requests feel free to submit an issue or pull-request to improve current support.
Spinetail
A Swift package for interfacing with your Mailchimp account, audiences, campaigns, and more.
Table of Contents
🎬 Introduction
Spinetail is a Swift package for interfacing with your Mailchimp account, audiences, campaigns, and more.
Built on top of the code generated by Swaggen by Yonas Kolb from Mailchimp’s OpenAPI Spec and optimized.
What’s a Spinetail?
A Spinetail is a type of Swift bird which shares it’s habitat with chimps (such as the chimp in Mailchimp).
How to create and send an email campaign
🎁 Features
Here’s what’s currently implemented with this library:
… and more
🏗 Installation
To integrate Spinetail into your project using SPM, specify it in your Package.swift file:
Spinetail uses
URLSession
for network communication via Prch.However if you are building a server-side application in Swift and wish to take advantage of SwiftNIO, then you’ll want import PrchNIO package as well:
PrchNIO adds support for
EventLoopFuture
and using the networking infrastructure already supplied by SwiftNIO.If you are using Vapor, then you may also want to consider using SpinetailVapor package:
The SpinetailVapor package adds helper properties and methods to help with setting up and accessing the
Prch.Client
.Setting Up Your Mailchimp Client with Prch
In order to get started with the Mailchimp API, make sure you have created an API key. Typically the API key looks something like this:
Once you have that, decide what you’ll be using for your session depending on your platform:
URLSession
- iOS, tvOS, watchOS, macOS from PrchAsyncHTTPClient
- Linux/Server from PrchNIOVapor.Client
- Vapor from PrchVaporHere’s an example for setting up a client for Mailchimp on a standard Apple platform app:
If you are using Vapor then you’ll want to configure your client inside your application configuration:
… then you’ll have access to it throughout your application and in your requests:
Now that we have setup the client, we’ll be using let’s begin to access the Mailchimp API.
💪 Usage
🕊 Prch Basics
To make a request via
Prch
, we have three options using ourclient
:Closure-based Completion
Async/Await
Synchronous
In each case there are possible results:
Let’s start with an example using audience member lists.
👩 Audience List Members
Getting an Audience List Member
According to the documentation for the Mailchimp API, we can get a member of our audience list based on their subscriber_hash. This is described as:
The means we can use:
contact_id
In our case, we’ll be using an email address to see if we have someone subscribed. Additionally we need our audience’s
listID
which is found on the audience settings page.With that email address, we can create a
Request
:As previously noted there are three ways to execute a call. In this case, let’s use the synchronous call:
This is a good example of where we’d want to handle a
404
. If the member is found, we may need to just update them, otherwise we want go ahead and add that subscriber.Adding new Audience List Members
To add a new audience member we need to create a
Lists.PostListsIdMembers.Request
:Now that we have a request let’s use the completion handler call for adding a new member:
Updating Existing Audience List Members
Let’s say our attempt to find an existing subscriber member succeeds but we need to update the member’s interests. We can get
subscriberHash
from our found member and theinterestID
can be queried.Putting it together in Vapor
Here’s an example in Vapor using Model Middleware provided by Fluent:
Now that we have an example dealing with managing members, let’s look at how to get a list of campaigns and email our subscribers in Swift.
📩 Templates and Campaigns
With newsletters there are campaigns and templates. Campaigns are how you send emails to your Mailchimp list. A template is an HTML file used to create the layout and basic design for a campaign. Before creating our own campaign and template, let’s look at how to pull a list of campaigns.
Pulling List of Campaigns
On the BrightDigit web site, I want to link to each newsletter that’s sent out. To do this you just need the
listID
again. We’ll be pulling up to 1000 sent campaigns sorted from last sent to first sent:To get the content we to grab it based on each campaign’s
campaignID
.Get Newsletter Content
Before grabbing the content, we need to grab the
campaignID
from the campaign:Creating a Template
To actually send we need to create an template using the
POST
request. Here’s an example with async and await:Let’s use the template to create a campaign and send it.
Send an Campaign Email to Our Audience List
📞 Requests
List of APIs and the status of their support. If you have any requests feel free to submit an issue or pull-request to improve current support. For more information on the Mailchimp Marketing API, checkout their API documentation.
😁 Fully Supported
Due to the limitation of existing 32-bit watchOS devices, the library need to exclude certain APIs to limit size. Therefore these sets of APIs are available on all operating systems and platforms including watchOS.
Campaigns
Lists
Templates
Testing Pending
Template Folders
Search Campaigns
Search Members
Reports
Root
😊 Pending Next Support
These are the next set of API for which migrating to watchOS is desired as well as more robust testing and documentation. If you have any requests feel free to submit an issue or pull-request to improve current support.
File Manager
Batches
Automations
😌 Remaining Requests
These are the least priority set of API for which migrating to watchOS as well as robust testing and documentation have been prioritized. If you have any requests feel free to submit an issue or pull-request to improve current support.
Activity Feed
Authorized Apps
Connected Sites
Conversations
Customer Journeys
Ecommerce Stores
Facebook Ads
Landing Pages
Verified Domains
🙏 Acknowledgments
Thanks to Yonas Kolb for his work on a variety of project but especially Swaggen.
📜 License
This code is distributed under the MIT license. See the LICENSE file for more info.