Migrate from beehiiv to Unitpost

Contacts, suppressions, topics, templates and more come over in one import.
What comes with you
To leave beehiiv, paste a beehiiv API key into Unitpost's Migration Assistant. It imports subscribers with their status, custom fields and paid tier, publications and newsletter lists as topics, segments and tags, post templates, drafts and your 25 most recent posts as templates, and webhooks. Unconfirmed subscribers are left out. Copy beehiiv's suppression list across by hand first, since its API can't read it. The website, referrals, ad network and paid subscriptions stay at beehiiv.
| Data | Result | What happens |
|---|---|---|
| Contacts | Imports | Subscribers of every publication the key can see import, merged into one contact per address. beehiiv has no name fields, so custom fields named First Name and Last Name become the name and the rest become custom fields. A paid subscriber's tier is kept in a beehiiv_tier field. |
| Unsubscribes and consent | Imports | Inactive and invalid subscribers import unsubscribed; with several publications, a reader is unsubscribed only if they left all of them. Paused subscribers import subscribed. Subscribers beehiiv still shows as pending or awaiting approval never confirmed, and are left out. |
| Topics | Imports | With more than one publication, each becomes an opt-in topic and readers keep the ones they subscribed to. Newsletter lists become topics too, with their members; a list beehiiv subscribes everyone to becomes an opt-out topic here. |
| Suppressions (bounces and complaints) | Partly imports | Invalid addresses import as bounce suppressions, and spam complainers arrive unsubscribed because beehiiv marks them inactive. Hard bounces stay active in beehiiv's API and sit on a suppression list it can't read, so step 1 copies that list by hand. |
| Segments and tags | Partly imports | Segments come over as a static snapshot of their members at beehiiv's last calculation, up to 10,000 people each. Tags become static segments when beehiiv's list endpoint returns them; the preview says when it didn't. |
| Templates | Partly imports | Post templates, drafts and your 25 most recent posts import as code-mode drafts of beehiiv's rendered email HTML, not as editable blocks. Web-only content, polls and paywalled sections don't carry over. |
| Sending domains | Partly imports | beehiiv has no domains API. Domains come from the sender addresses on your lists, posts and post templates, and are recreated as verifying with new DKIM keys. |
| Webhooks | Partly imports | Webhooks, on paid beehiiv plans, import switched off with a new signing secret. Subscription events map to contact.*; post, survey, tier and podcast events are not mapped. |
| API keys | Rebuild | beehiiv keys stay at beehiiv. Create Unitpost API keys instead, and delete the beehiiv key you used for the import. |
| Send history and analytics | Stays behind | Post stats, subscriber activity and posts older than the 25 most recent stay at beehiiv. |
| Automations | Stays behind | beehiiv's API shares automation names and triggers only, which the preview lists. Rebuild them in Unitpost Automations, which are in early access, or send those emails from your code. |
| Website, growth tools and paid subscriptions | Stays behind | The website, referral program, Boosts, ad network, polls and paid subscriptions stay at beehiiv. Unitpost doesn't bill readers, so paid subscriptions stay with beehiiv or your payment tool. |
Before you start
- A beehiiv API key: Settings → Workspace Settings → API → Create New API Key. Only an Owner or Admin can create one, and beehiiv asks you to finish Stripe Identity Verification first.
- A beehiiv key sees every publication in the workspace unless it was limited to some, which paid plans allow. Only the publications it sees import.
- beehiiv keys carry every permission; there is no read-only key. The importer only reads; delete the key when the move is done.
- beehiiv's Free plan allows 30 API requests a minute, so a large list imports slowly. Leave the job running.
- A Unitpost API key for your app with
emails:send, pluscontacts:writeandtopics:writeif your signup form adds contacts and opts them into a publication's topic, created in API keys. - A second Unitpost key for the migration steps, with
suppressions:write,templates:writeandwebhooks:manage, saved asUNITPOST_MIGRATION_KEY. Keep it out of your app and revoke it once the move is done. - Access to your DNS host.
The cut-over plan
beehiiv keeps sending until the last step, so there is no gap in delivery.
Copy beehiiv's suppression list
beehiiv keeps hard bounces, repeated delivery failures, spam complaints and manual blocks on a Suppression list (Subscribers → Suppression list). Its API can't read the list and its help page describes no export, so copy the addresses into a file, one per line. Paste them into Suppressions or add them with
POST /api/v1/suppressionsbefore you import; that also makes those contacts arrive unsubscribed.copy-suppressions.mjsimport { readFileSync } from "node:fs"; import { Unitpost } from "unitpost"; const unitpost = new Unitpost(process.env.UNITPOST_MIGRATION_KEY); // One address per line, copied from beehiiv's Suppression list. const emails = readFileSync("beehiiv-suppressed.txt", "utf8") .split(/\r?\n/) .map((line) => line.trim().toLowerCase()) .filter(Boolean); // Up to 1,000 per call. reason "import" makes the contact import mark them unsubscribed. for (let i = 0; i < emails.length; i += 1000) { const { error } = await unitpost.suppressions.create({ emails: emails.slice(i, i + 1000), reason: "import", }); if (error) throw new Error(error.message); }Run the Migration Assistant
Paste the key in Settings → Migration. The preview lists your publications, the subscribers it leaves out, paused and paid subscribers, your automations, and whether tags came back.
Bring tags by CSV if they didn't come back
If the preview says tags didn't come over, export subscribers as CSV from beehiiv, import the tags column in Contacts as a custom field, and build a dynamic segment per tag with a
containscondition on that field.Verify your domain
Open Domains and publish the records shown: MX and SPF on
unitpost.<your domain>and a DKIM TXT under_domainkey. They don't replace beehiiv's records, so both stay authenticated while you move. Keep your single_dmarcrecord.Rewrite merge tags and publish
{{First Name | Reader}}is now{{first_name}}with the fallback dropped, and{{email}},{{unsubscribe_url}}and your custom fields map by name. beehiiv-only tags such as{{rp_refer_url}},{{publication_name}}and{{subscriber_id}}stay as written for you to replace. Posts arrive as beehiiv's rendered HTML; publish each template you keep withPATCH /api/v1/email/templates/{id}.Point signups at Unitpost
Your beehiiv website and forms keep adding readers to beehiiv. Send signups from your own form with
POST /api/v1/contacts. beehiiv'scustom_fieldslist of{ name, value }becomes acustom_fieldsobject, andnewsletter_list_idsbecomes a topic subscription on the contact.signup.ts// Before: beehiiv await fetch("https://api.beehiiv.com/v2/publications/" + PUBLICATION_ID + "/subscriptions", { method: "POST", headers: { Authorization: "Bearer " + process.env.BEEHIIV_API_KEY, "content-type": "application/json" }, body: JSON.stringify({ email: "user@example.com", custom_fields: [{ name: "First Name", value: "Ada" }], newsletter_list_ids: [LIST_ID], }), }); // After: Unitpost import { Unitpost } from "unitpost"; const unitpost = new Unitpost(process.env.UNITPOST_API_KEY); const { error } = await unitpost.contacts.create({ email: "user@example.com", first_name: "Ada" }); // 409 conflict: the contact already exists, which is fine for a signup. if (error && error.code !== "conflict") throw new Error(error.message); // A newsletter list became a topic: record that the reader chose it. await unitpost.email.topics.setTopic("user@example.com", { topic_id: "top_123", subscribed: true });Re-key and enable webhooks
beehiiv delivers webhooks through Svix, and Unitpost uses the same
svix-id,svix-timestampandsvix-signatureheaders, so your handler needs the newwhsec_secret and the new event names. Rotate the secret in Webhooks to see it, then setstatustoenabled.Test, move traffic, then delete the key
Send each template with
POST /api/v1/email/templates/{id}/test-sendand firePOST /api/v1/webhooks/{id}/testat each endpoint. Send your next post from Unitpost as a campaign to a small segment first, then to the topic. Delete the beehiiv key, and keep beehiiv only for what you still use there.
Name changes
Same ideas, new names
| beehiiv | Unitpost | Note |
|---|---|---|
| Publication | Topic (opt-in) | When the key sees more than one; a single publication is the whole list. |
| Subscription | Contact | |
| Newsletter list | Topic | |
| Segment | Segment (static) | Imported as a snapshot; rebuild dynamic ones as filters. |
| Tag | Segment (static) | |
| Custom field | Contact field | |
| Post | Campaign | |
| Post template | Template | |
| Premium tier | beehiiv_tier contact field | Unitpost doesn't bill readers. |
| Suppression list | Suppressions | Copied by hand. |
{{First Name | Reader}} | {{first_name}} |
API calls
| beehiiv API | Unitpost API | Note |
|---|---|---|
| POST /v2/publications/{publicationId}/subscriptions | POST /api/v1/contacts | |
| GET /v2/publications/{publicationId}/subscriptions | GET /api/v1/contacts | |
custom_fields: [{ name, value }] | custom_fields: { key: value } | |
newsletter_list_ids | PUT /api/v1/contacts/{id}/topics | |
| GET /v2/publications/{publicationId}/segments | GET /api/v1/segments | |
| GET /v2/publications/{publicationId}/newsletter_lists | GET /api/v1/email/topics | |
| GET /v2/publications/{publicationId}/posts | GET /api/v1/email/campaigns | |
| GET /v2/publications/{publicationId}/post_templates | GET /api/v1/email/templates | |
| POST /v2/publications/{publicationId}/webhooks | POST /api/v1/webhooks | |
Authorization: Bearer | Authorization: Bearer <api key> | Same header, Unitpost key. |
Webhook events
| beehiiv event | Unitpost event | Note |
|---|---|---|
| subscription.created | contact.created | |
| subscription.confirmed, subscription.upgraded, subscription.downgraded | contact.updated | |
| subscription.paused, subscription.resumed | contact.updated | |
| subscription.deleted | contact.deleted | |
| newsletter_list_subscription.subscribed, newsletter_list_subscription.unsubscribed | topic.subscribed, topic.unsubscribed | The importer maps them to contact.updated; the topic events are the closer match. |
| post.sent | email.sent | Ours fires per recipient. Not mapped by the importer. |
| survey.*, podcasts.*, subscription.tier.* | No equivalent | Not mapped by the importer. |
Before you cancel beehiiv
- Copy the Suppression list once more and add any new addresses here.
- Export subscribers as CSV, including the unconfirmed ones the importer leaves out.
- Move paid subscriptions to your payment tool, or keep beehiiv for them.
- Keep or redirect your beehiiv website and its archive; Unitpost doesn't host one.
- Delete beehiiv's DNS records once no mail goes through beehiiv. Keep
_dmarcand Unitpost's records. - Delete the beehiiv API key you used for the import.
Facts about beehiiv checked 2026-11-05. Providers change their APIs and plans; check their docs before you rely on a detail.
Questions about leaving beehiiv
Why do I have to copy suppressions by hand?
What happens with several publications?
I can't find the button to create a beehiiv API key.
Can I keep my paid subscribers?
Leaving beehiiv?
Preview the migration on the free tier
Verify a domain, paste an API key, and see what would come across before anything is written.
