Migrate from beehiiv to Unitpost

beehiiv subscribers importing with one API key, and the suppression list its API can't read copied across by hand

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.

DataResultWhat happens
ContactsImportsSubscribers 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 consentImportsInactive 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.
TopicsImportsWith 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 importsInvalid 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 tagsPartly importsSegments 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.
TemplatesPartly importsPost 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 domainsPartly importsbeehiiv 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.
WebhooksPartly importsWebhooks, 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 keysRebuildbeehiiv keys stay at beehiiv. Create Unitpost API keys instead, and delete the beehiiv key you used for the import.
Send history and analyticsStays behindPost stats, subscriber activity and posts older than the 25 most recent stay at beehiiv.
AutomationsStays behindbeehiiv'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 subscriptionsStays behindThe 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.
  • 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

beehiiv keeps sending until the last step, so there is no gap in delivery.

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

    copy-suppressions.mjs
    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. 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 as a custom field, and build a dynamic segment per tag with a contains condition on that field.

  4. 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 _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.

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

Name changes

Same ideas, new names

beehiivUnitpostNote
PublicationTopic (opt-in)When the key sees more than one; a single publication is the whole list.
SubscriptionContact
Newsletter listTopic
SegmentSegment (static)Imported as a snapshot; rebuild dynamic ones as filters.
TagSegment (static)
Custom fieldContact field
PostCampaign
Post templateTemplate
Premium tierbeehiiv_tier contact fieldUnitpost doesn't bill readers.
Suppression listSuppressionsCopied by hand.
{{First Name | Reader}}{{first_name}}

API calls

beehiiv APIUnitpost APINote
POST /v2/publications/{publicationId}/subscriptionsPOST /api/v1/contacts
GET /v2/publications/{publicationId}/subscriptionsGET /api/v1/contacts
custom_fields: [{ name, value }]custom_fields: { key: value }
newsletter_list_idsPUT /api/v1/contacts/{id}/topics
GET /v2/publications/{publicationId}/segmentsGET /api/v1/segments
GET /v2/publications/{publicationId}/newsletter_listsGET /api/v1/email/topics
GET /v2/publications/{publicationId}/postsGET /api/v1/email/campaigns
GET /v2/publications/{publicationId}/post_templatesGET /api/v1/email/templates
POST /v2/publications/{publicationId}/webhooksPOST /api/v1/webhooks
Authorization: BearerAuthorization: Bearer <api key>Same header, Unitpost key.

Webhook events

beehiiv eventUnitpost eventNote
subscription.createdcontact.created
subscription.confirmed, subscription.upgraded, subscription.downgradedcontact.updated
subscription.paused, subscription.resumedcontact.updated
subscription.deletedcontact.deleted
newsletter_list_subscription.subscribed, newsletter_list_subscription.unsubscribedtopic.subscribed, topic.unsubscribedThe importer maps them to contact.updated; the topic events are the closer match.
post.sentemail.sentOurs fires per recipient. Not mapped by the importer.
survey.*, podcasts.*, subscription.tier.*No equivalentNot 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. 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?

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.

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.