Migration

Moving your contacts from another platform

Export your list from the platform you are leaving and upload the file as it is. Names, tags and everyone who opted out come across. What needs care is the export itself, because each platform's file leaves something out.

Read this if you are moving a list off Mailchimp, Kit, beehiiv, Substack or another email platform. Skip it if your contacts live in your own product's database: a sync job over the API suits you better, and the last section says how.

Upload the export as it is

Export your contacts from the platform you are leaving and upload the file in the dashboard without editing it first. The exports of the platforms below are recognised by their columns, so the mapping arrives filled in with the columns the import knows: the email address, and the names and tags wherever the platform exports them. The import then reads what that platform's file says about each person: who is subscribed, who unsubscribed, who reported your email as spam and who bounced. Not every platform writes all four down, which is what the sections below are for.

Two questions come before anything is imported. One is a checkbox saying these people agreed to get email from you, and the import does not start without it. The other is when they last heard from you. The answer that they never have refuses the import. An answer of more than a year ago imports the list, and the first email to each contact it creates goes out in checked batches, transactional mail included, even on an account that has finished warming up.

Then every address that would come in subscribed is checked, and the import ends with a count of what came in and what was refused, and why. When anything was refused, there is a report to download.

Where a platform needs two files, import the one holding the opt-outs first. A contact that already exists keeps its status, so nobody in that file is subscribed here even for the minutes between the two imports.

What carries over

The file brings the people across and what the old platform knew about them.

  • Names. A first and a last name column, or a single full name, which fills whichever of the two the separate columns leave empty.
  • Tags. Mailchimp's TAGS column, Kit, beehiiv, Buttondown and EmailOctopus tags, Ghost labels, MailerLite groups and the Loops user group all become tags. An import adds tags and never removes one.
  • Opt-outs. Anyone who unsubscribed, reported spam or bounced on the old platform comes in as an unsubscribed contact and gets no marketing email from you. Unsubscribed contacts are stored free, so they take nothing from your plan.
  • Unconfirmed people stay out. Someone who never confirmed a double opt-in, or never agreed to marketing at all, is left out and listed in the report.
  • Existing contacts keep their status. An import never resubscribes anyone who unsubscribed or bounced, and it only moves the other way: a row that says someone left there unsubscribes a matching subscribed contact here, in this workspace only, and ends their sequences. A name in the file does replace the one the contact had, and an empty cell leaves it alone.

What stays behind, and what starts

Only the columns the mapping names, and the platform's own status columns, are read, so custom fields stay behind. Once the contacts exist, set those over the API, where a contact's properties are arbitrary JSON.

The old platform's automations do not come across in any file, because no two tools model branching the same way, so sequences are rebuilt here. A tag the import adds starts no tag-triggered sequence: these people have been through the old platform's automations already.

A sequence that starts when a contact is created is the exception. It starts for each new subscribed contact the import creates, so pause it before a large import, or filter its trigger to the sources you mean, unless you want the whole list to enter it at once.

What we check

Every address that would come in subscribed is checked, whatever the answers to the two questions were; opt-outs come in unsubscribed and skip the checks. A shared team mailbox such as info, sales or support is imported with a flag rather than refused, and so is an address whose domain could not be checked in time.

A refused row is not imported. The report lists each one with the line it sits on in your own file, the address, the reason and a sentence explaining it, and for a likely typo the corrected address. It is kept for 30 days, and the file you uploaded is deleted once the import has finished with it. A row is refused for any of these:

  • A dead domain: one that does not exist, or one that does not accept email.
  • A throwaway mailbox service.
  • A likely typo of one of the big mailbox providers. Fix the row from the report and it can go in with your next import.
  • A system mailbox, such as abuse, postmaster or no-reply.
  • An address that has already hard-bounced as nonexistent, anything that is not an email address, an empty email cell, or a second copy of an address that appears earlier in the file.
  • No room left: rows past your plan's contact limit.

Limits and the file itself

The size limits are checked before any row is imported, so a file over one is turned away whole rather than half imported.

  • Up to 50 MB and 100,000 rows in one file. A bigger list goes in as several files, one after another.
  • One import at a time in each workspace. It never blocks another workspace.
  • A CSV file with a header row. The separator can be a comma, a semicolon, a tab or a pipe, and a file saved by Excel on Windows reads correctly. A file saved as Unicode text is refused: save it as CSV instead.
  • Blank lines, and rows with every cell empty, are skipped.

Mailchimp

Open Audience, then All contacts, and choose Export Audience. The export arrives as a zip with a file for each group of people: subscribed, non-subscribed, unsubscribed, and cleaned, which is Mailchimp's word for addresses that bounced. Unzip it and import each file on its own, under the name Mailchimp gave it. The unsubscribed and cleaned files are recognised by their columns and come in unsubscribed, but the subscribed and non-subscribed files look alike, so the file name is what tells them apart, and a renamed one is asked about rather than guessed. Non-subscribed people never opted in, so they stay out. The TAGS column comes in as tags.

beehiiv

Open Settings, then Export Data, and choose Export All Subscribers. The file marks someone who unsubscribed or reported spam as inactive, and both come in unsubscribed. It does not mark bounces: an address that bounced still reads as active in beehiiv's file, so it comes in subscribed unless one of the checks above refuses it. Tags come in as tags.

Kit

Open Subscribers, set the status filter to All, select everyone and export. Kit's default export holds confirmed subscribers only, so an export made without choosing All leaves out the people who unsubscribed, and they cannot come in unsubscribed if they are not in the file. Tags come in as tags.

Substack

Open Settings, then Import/Export, and start a new export, then use the file in the zip whose name starts with email_list; a reader it marks with email disabled comes in unsubscribed. Subscribers, then Export, works too. Either way, Substack leaves unsubscribed free readers out of its exports, so those readers cannot come across.

Ghost

Open Members, then the menu, and choose Export all members. Ghost's file marks neither bounces nor spam complaints, so export the members under the Email disabled filter as a second file and import it as Bounced, which brings those people in unsubscribed. Labels come in as tags.

Buttondown

Open Subscribers and choose Export. Unsubscribed, complained and undeliverable subscribers come in unsubscribed, unactivated ones stay out, and tags come in as tags.

Loops

Open Audience, then the menu, and choose Download. Anyone Loops has as unsubscribed comes in unsubscribed, and the user group comes in as a tag.

Klaviyo

Open Lists & segments, choose the list, and export it to CSV with Email Marketing Consent ticked. That column is what says who agreed to marketing email: a file without it is asked about, and the better fix is to export it again with the box ticked. A list export also leaves out that list's unsubscribes, so export the suppressed profiles as well, from Profiles, then Suppressed profiles, then Export CSV, and import that file too: everyone in it comes in unsubscribed. Profiles Klaviyo holds as never subscribed stay out.

Customer.io

Open People, choose Export to CSV, and include All attributes. Someone who unsubscribed comes in unsubscribed. Bounces and spam complaints are not marked in the people export, so they cannot come across from it, and a person who bounced there comes in subscribed unless one of the checks above refuses the address.

AWeber

Open Subscribers, choose Show All, and export to CSV. A subscriber with a stop time, which is how AWeber marks someone who left, comes in unsubscribed, and a full name is split into first and last.

MailerLite

Open Subscribers, pick a view, select everyone and export to CSV, one file per view. The file does not say who is in it, so the import asks which contacts you exported: active subscribers, unsubscribed, spam complaints, bounced or cleaned, or unconfirmed. Import each view as its own file, because a file that mixes them cannot be told apart. Groups come in as tags.

Brevo

Open Contacts, select all contacts and export, with one export for each blocklist reason. Like MailerLite's, the file does not say who is in it, so the import asks, and each export goes in on its own.

EmailOctopus

Open a list's contacts, filter them by status, export, and unzip the download. The file does not say who is in it either: the import asks which status you exported, so export and import one status at a time. Tags come in as tags.

Any other platform

A file from any other platform is read as a spreadsheet: you choose the email column, and the name and tag columns if it has them.

When the API is the better path

An upload is a snapshot. If your contacts live in your own product's database and keep changing, keep SendHeron in step with the contact upsert endpoint instead. It matches on email address and is idempotent, so a sync job can run again after a failure without creating duplicates, and it attaches tags by name. There is no file import over the API.

Addresses that arrive over the API are checked too. One that cannot receive mail is still created rather than refused, takes no place on your plan, and every send to it comes back suppressed with ADDRESS_UNDELIVERABLE, so a sync job has nothing to filter out first.

PUT /contacts, one call per contact
curl -X PUT https://api.sendheron.com/api/v1/contacts \
  -H 'Authorization: Bearer <YOUR_API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{ "email": "[email protected]", "firstName": "Ada",
        "tagNames": ["customer"], "source": "sync" }'

When this does not apply

Do not import a list you bought, rented or scraped: the anti-spam policy treats uploading one as grounds for suspension. Nor a list of people who never heard from you, which the import refuses when you say so. A list you last mailed more than a year ago can come in, but its first emails go out in checked batches rather than all at once, and a batch that bounces or draws complaints past the line holds the rest of that import until a person here has looked at it. Do not plan a launch-day send to an old list.

Questions

Do I need to clean the list before I upload it?
No. Upload the export as it is. Every address that would come in subscribed is checked, refused rows land in the report with the reason, and opt-outs come in unsubscribed. What does need care is the export itself: a file that leaves out the people who unsubscribed cannot bring them across, and each platform's section above says what its export leaves out.
What happens to contacts I already have?
They are matched by email address and keep their status, so an import never resubscribes anyone who unsubscribed or bounced. A name in the file replaces the one on the contact, and an empty cell leaves it alone. A row whose platform says the person left unsubscribes a matching subscribed contact and ends their sequences, in this workspace only.
Will an import start my sequences?
Not through its tags: a tag added by an import starts no tag-triggered sequence. A sequence that starts when a contact is created does start for each new subscribed contact, so pause it, or filter its trigger to the sources you mean, before a large import.
Do the unsubscribed contacts count toward my plan?
No. Contacts that come in unsubscribed are stored free, like every unsubscribed contact, and an address the checks refuse is not imported at all. The plan counts the subscribed contacts you can actually mail.

Read next

Free trial

Guides are free. The API needs an account.

Start a trial and follow this one end to end, against your own workspace and real sending.

7-day free trial. Pick a plan, add a card, cancel before the trial ends and you won't be charged. 1,000 contacts and 5,000 sends during the trial.