Skip to content

Migrating from Mailchimp

Connect Mailchimp, read a dry run of exactly what would come across, then commit it. Nothing is written to your contacts until you commit. Mailchimp data streams through our importer in memory and goes straight into your tables; the Mailchimp credential is kept in your account’s SSM Parameter Store during the import and deleted when you commit.

Mailchimp is a trademark of its owner, and we aren’t affiliated with it.

Create a Mailchimp API key under Profile → Extras → API keys (Mailchimp’s guide: About API keys). Then start the import. Leave out audienceIds to import every audience.

Terminal window
curl -X POST "$BASE/imports/mailchimp" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"method":"apiKey","apiKey":"<mailchimp-key>","audienceIds":["a1b2c3d4e5"],"filters":{"engagedWithinDays":365,"minRating":2},"importTemplates":true}'

A key Mailchimp rejects gives 422 mailchimp_auth_failed. To connect with OAuth instead, send {"method":"oauth"} and open the authorizeUrl in the response. Mailchimp sends you back to our one redirect URL, https://api.rhinorelay.com/oauth/mailchimp/callback, which hands you straight on to your own Region. There we store the token in your account, and the import starts.

The link works for an hour. Until the import connects, reading it (GET /imports/{id}) with a key that can start imports (contacts:write or admin) returns a fresh authorizeUrl, so you can finish later; a read-only key gets none. An import nobody connects within a day fails, and its links stop working: start a new one.

The import reads your members and their activity in your last 12 campaigns. Poll it until state is dry_run_ready. We also publish an import.dry_run_ready event to your EventBridge bus at that point.

If Mailchimp refuses the credential or a request (an audience ID that doesn’t exist, for example), the import’s state becomes failed, and its failureReason says why: mailchimp_refused for the credential, mailchimp_not_found for something that doesn’t exist, mailchimp_rejected for any other request Mailchimp turns down. An import nobody connected within a day has mailchimp_not_connected, and the API reference lists the rest. Start a new one. Rate limits, timeouts and Mailchimp outages are retried, carrying on where the import left off.

Terminal window
curl "$BASE/imports/<im_...>" -H "Authorization: Bearer $KEY"

The summary tells you:

Field Means
read Members read from Mailchimp
toImport Contacts that will be written
suppressed Of those, how many come across unsubscribed or cleaned, so they stay suppressed
removedByCleaning Subscribed members the filters leave out
skipped Members not imported, by reason: pending, transactional, invalid_email, unsendable_address (non-ASCII characters before the @, which Amazon SES can’t send to) and erased
errors Problems, without addresses

If the numbers look right, commit. You can change the filters here, without reading Mailchimp again:

Terminal window
curl -X POST "$BASE/imports/<im_...>/commit" -H "Authorization: Bearer $KEY" -H 'Idempotency-Key: mailchimp-commit-1' \
-H 'Content-Type: application/json' -d '{"filters":{"engagedWithinDays":365,"minRating":2}}'
curl "$BASE/imports/<im_...>" -H "Authorization: Bearer $KEY"

Check it worked: the import’s state is completed, and GET $BASE/contacts lists the imported contacts. Then revoke the API key in Mailchimp (or our app’s access, with OAuth): we’ve already deleted our copy.

Mailchimp status Imported as Why
subscribed subscribed Opted in
unsubscribed unsubscribed Must stay suppressed
cleaned cleaned Hard-bounced
archived unsubscribed, with the reason mailchimp_archived A safe default. You can resubscribe them one by one with new consent.
pending Not imported Never confirmed
transactional Not imported Never opted into marketing
  • Merge fields become custom fields: FNAME becomes fields.FNAME.
  • Tags come across as tags, and interests as tags named interest:<category>:<name>.
  • Opt-in data (timestamp_opt, ip_opt, timestamp_signup, ip_signup, source and marketing permissions) becomes the consent record, with method imported. A member with neither timestamp_opt nor timestamp_signup gets its last_changed time as consent.at.
  • Engagement: the member rating sets the starting score, and the last open and click come from your recent campaigns.
  • The same address in several audiences comes across once: the most restrictive status wins, tags are combined, and the earliest consent is kept.
  • Erased addresses are never imported again.
  • Templates: with importTemplates, the HTML of your last 10 campaigns becomes templates, with *|FNAME|* merge tags rewritten as Liquid. Each is flagged for review: check the unsubscribe link and footer before you use it.

Filters apply only to subscribed members. Suppressed members always come across, so they stay suppressed.

Filter Default Keeps subscribed members who
engagedWithinDays 365 opened, clicked or opted in within this many days
minRating 2 have a Mailchimp member rating of at least this (1 to 5)
includeUnengaged false set to true to keep everyone, whatever their engagement

Members the filters leave out aren’t imported at all. Sending to people who haven’t engaged in a year is the fastest way to bounces and complaints.

An import that hasn’t been committed can be cancelled. Its credential and any staging files are deleted.

Terminal window
curl -X POST "$BASE/imports/mailchimp" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"method":"apiKey","apiKey":"<mailchimp-key>"}'
curl -X DELETE "$BASE/imports/<im_...>" -H "Authorization: Bearer $KEY"

Check it worked: the delete returns 204, and the import’s state is cancelled. A committed import gives 409 already_committed.

For a CSV export from Mailchimp or anywhere else, see CSV import.