---
title: "Migrate from beehiiv to Unitpost"
description: "Leave beehiiv in order: copy the suppression list by hand, import subscribers, publications, segments and posts, rewrite merge tags, then move signups."
url: https://www.unitpost.com/migration/beehiiv
section: Migration
updated: 2026-11-07
---
# Migrate from beehiiv to Unitpost

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.

Contacts, suppressions, topics, templates and more come over in one import.

## What comes with you

| 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`, plus `contacts:write` and `topics:write` if your signup form adds contacts and opts them into a publication's topic, created in [API keys](/dashboard/api-keys).
- A second Unitpost key for the migration steps, with `suppressions:write`, `templates:write` and `webhooks:manage`, saved as `UNITPOST_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

### 1. 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](/dashboard/suppressions) or add them with `POST /api/v1/suppressions` before you import; that also makes those contacts arrive unsubscribed.

```ts
import { 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);
}
```

### 2. Run the Migration Assistant

Paste the key in [Settings → Migration](/dashboard/settings/migration). The preview lists your publications, the subscribers it leaves out, paused and paid subscribers, your automations, and whether tags came back.

### 3. 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](/dashboard/contacts) as a custom field, and build a dynamic segment per tag with a `contains` condition on that field.

### 4. Verify your domain

Open [Domains](/dashboard/email/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 `_dmarc` record.

### 5. 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 with `PATCH /api/v1/email/templates/{id}`.

### 6. 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's `custom_fields` list of `{ name, value }` becomes a `custom_fields` object, and `newsletter_list_ids` becomes a topic subscription on the contact.

```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 });
```

### 7. Re-key and enable webhooks

beehiiv delivers webhooks through Svix, and Unitpost uses the same `svix-id`, `svix-timestamp` and `svix-signature` headers, so your handler needs the new `whsec_` secret and the new event names. Rotate the secret in [Webhooks](/dashboard/webhooks) to see it, then set `status` to `enabled`.

### 8. Test, move traffic, then delete the key

Send each template with `POST /api/v1/email/templates/{id}/test-send` and fire `POST /api/v1/webhooks/{id}/test` at 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.

## 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 `_dmarc` and Unitpost's records.
- Delete the beehiiv API key you used for the import.

Facts about beehiiv checked 2026-11-05.

## FAQ

### Why do I have to copy suppressions by hand?

beehiiv keeps hard-bounced addresses on a Suppression list that its API can't read, and those readers still show as active. Without the list, your first Unitpost send would bounce them again. Copy it across before you import.

### What happens with several publications?

They import into one Unitpost workspace, with each publication as an opt-in topic. Readers keep exactly the publications they subscribed to, and someone is unsubscribed only if they left all of them. Send each newsletter as a campaign scoped to its topic.

### I can't find the button to create a beehiiv API key.

beehiiv shows it only to an Owner or Admin, and only after the account finishes Stripe Identity Verification. Ask the workspace owner, or complete the verification first.

### Can I keep my paid subscribers?

They import as contacts with their tier in a custom field, so you can send premium posts to a segment of them. Billing stays with beehiiv or your payment tool, because Unitpost doesn't charge readers.

## Related

- [Migrate from Klaviyo to Unitpost](https://www.unitpost.com/migration/klaviyo): Leave Klaviyo in order: import profiles, consent, lists, segments and templates with a Read-Only key, rebuild flows, then move your signups and sends.
- [Migrate from Resend to Unitpost](https://www.unitpost.com/migration/resend): Leave Resend in order: copy its suppression list, import contacts, topics and templates, re-verify DNS, swap the SDK and re-key svix webhooks.
- [Migrate from SendGrid to Unitpost](https://www.unitpost.com/migration/sendgrid): Leave SendGrid in order: import contacts and suppressions, copy unsubscribe-group opt-outs, re-verify DNS, port dynamic templates, swap the webhook verifier.
