---
title: "Migrate from Klaviyo to Unitpost"
description: "Leave Klaviyo in order: import profiles, consent, lists, segments and templates with a Read-Only key, rebuild flows, then move your signups and sends."
url: https://www.unitpost.com/migration/klaviyo
section: Migration
updated: 2026-11-07
---
# Migrate from Klaviyo to Unitpost

To leave Klaviyo, paste a Klaviyo Read-Only private key into Unitpost's Migration Assistant. It imports profiles with their email consent and dated SMS consent, custom properties and location, lists and segments as today's members, bounces and manual suppressions, saved templates, drafts and your 25 most recent campaigns with Klaviyo tags converted, and your sending domain. Profiles with no email are left out. Flows, segment filters, webhooks, forms and analytics stay at Klaviyo.

Contacts, suppressions, lists, segments and more come over in one import.

## What comes with you

| Data | Result | What happens |
| --- | --- | --- |
| Contacts | Imports | Every profile with an email address imports with first and last name, custom properties, organization, title, locale and location (IP address excluded). Profiles with only a phone number, or no identifier at all, are left out: a contact here needs an email. |
| Unsubscribes and consent | Imports | Subscribed profiles import subscribed, with the date and method Klaviyo recorded kept as contact fields. Unsubscribed profiles import unsubscribed. Profiles that never subscribed (often checkout-only customers, whom Klaviyo can email on implied consent) import unsubscribed from marketing: campaigns skip them, transactional email still reaches them. |
| Suppressions (bounces and blocks) | Imports | Hard bounces, invalid addresses, spam complaints and manual suppressions on a profile import as suppressions. Klaviyo shows many spam complaints only as an unsubscribe, so those arrive unsubscribed rather than blocked; to block them for every kind of email, export Klaviyo's suppressed profiles and import that file too. |
| SMS numbers and consent | Imports | A profile's phone number becomes the contact's SMS number. An SMS opt-in imports only with the date Klaviyo recorded for it, and an SMS opt-out always carries over. Klaviyo requires express SMS consent, so most opt-ins arrive with their date. |
| Lists | Imports | Each list becomes a static segment with its members. Someone who unsubscribed from one list only is left out of that segment. |
| Segments | Partly imports | Active segments come over with today's members as fixed segments; their filters use Klaviyo's own events and properties, so rebuild the filter here if a segment should keep updating. Inactive segments are listed, and very large segments may come as names only. |
| Templates | Partly imports | Saved templates, draft campaigns and your 25 most recent sent email campaigns import as code-mode drafts, with exact Klaviyo tags rewritten. Klaviyo may return no body for drag-and-drop templates; those arrive empty and are listed, so export their HTML from Klaviyo. |
| Sending domains | Partly imports | Klaviyo's sending-domain API is beta-only. The domain of your default sender address is recreated here as verifying, with new DKIM keys. |
| Topics | Rebuild | Klaviyo has no subscription topics in its API. Create topics here if you want per-category opt-outs. |
| Webhooks | Rebuild | Klaviyo's API truncates webhook URLs and never returns their secrets, so the preview names your webhooks and their topics for you to add here. Only some Klaviyo plans have webhooks. |
| API keys | Rebuild | Klaviyo keys stay at Klaviyo. Create Unitpost API keys instead, and delete the Klaviyo key you used for the import. |
| Flows | Stays behind | Flows don't move. The preview lists each one with its trigger and status; rebuild them in Unitpost Automations, which are in early access, or send from your code when the event happens. |
| Send history, analytics and forms | Stays behind | Campaign and flow stats, events, predictive analytics, sign-up forms and older sent campaigns stay at Klaviyo. Unitpost has no hosted forms or store integrations. |

## Before you start

- A Klaviyo private API key with Read-Only access: Settings → API keys → Create Private API Key, then Read-Only Key. Owners, Admins and Managers can create one. The six-character public key (site ID) can't read your data.
- Klaviyo shows a private key once; copy it before closing the dialog.
- If you text contacts: SMS on Unitpost is on paid plans only, and marketing texts need consent on record per contact.
- A Unitpost API key for your app with `emails:send`, plus `contacts:write` and `segments:write` if your signup form adds contacts, created in [API keys](/dashboard/api-keys).
- A second Unitpost key for the migration steps, with `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. Run the Migration Assistant

Paste the Read-Only key in [Settings → Migration](/dashboard/settings/migration). The preview counts what comes over and lists what doesn't: profiles with no email, never-subscribed profiles, your flows, large or inactive segments, and templates whose tags need review. Suppressions are written before contacts, so bounced and blocked addresses land suppressed.

### 2. 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 Klaviyo's records, so both stay authenticated while you switch. Keep your single `_dmarc` record.

### 3. Review tags and publish

`{{ first_name }}` is now `{{first_name}}`, `{{ person|lookup:'Favorite Color' }}` becomes the imported field `{{favorite_color}}`, and `{% unsubscribe %}` becomes a link to `{{unsubscribe_url}}`. Filters such as `|default:'friend'` are dropped. Unitpost fills flat `{{name}}` placeholders only, so `{% if %}` and `{% for %}` blocks, event data and tags such as `{% web_view %}` or `{{ organization.name }}` stay as written for you to rewrite. Publish with `PATCH /api/v1/email/templates/{id}`.

### 4. Rebuild segments and flows

Recreate the segments that should keep updating in [Segments](/dashboard/segments) as dynamic segments on contact fields. Rebuild each flow in Automations if your workspace has early access; until then, send from your code when the event happens, with `POST /api/v1/email`.

### 5. Point signups and events at Unitpost

Klaviyo forms keep adding profiles to Klaviyo. Send signups from your own form with `POST /api/v1/contacts` and add the contact to a segment where you used a list. Klaviyo's profile import upserts; here an existing contact answers `409`, and `PATCH /api/v1/contacts/{id or email}` updates it. Events you sent to Klaviyo to start flows become direct sends.

```ts
// Before: Klaviyo
const klaviyo = {
  Authorization: "Klaviyo-API-Key " + process.env.KLAVIYO_PRIVATE_KEY,
  revision: "2026-10-15",
  "content-type": "application/vnd.api+json",
};
const res = await fetch("https://a.klaviyo.com/api/profile-import", {
  method: "POST",
  headers: klaviyo,
  body: JSON.stringify({ data: { type: "profile", attributes: { email: "user@example.com", first_name: "Ada" } } }),
});
const { data: profile } = await res.json();
await fetch("https://a.klaviyo.com/api/lists/" + LIST_ID + "/relationships/profiles", {
  method: "POST",
  headers: klaviyo,
  body: JSON.stringify({ data: [{ type: "profile", id: profile.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. Klaviyo's call upserts; update with contacts.update.
if (error && error.code !== "conflict") throw new Error(error.message);
await unitpost.segments.addMember("seg_123", { contact: "user@example.com" });
```

### 6. Check SMS consent before texting

Numbers whose opt-in had no date arrived without consent. Record consent where the contact gave it (`POST /api/v1/contacts/{id}/sms-consent`) or ask them to confirm with `POST /api/v1/contacts/{id}/sms-opt-in-request`. Klaviyo SMS opt-outs came over and stay in force.

### 7. Add webhooks

Add each endpoint the preview listed in [Webhooks](/dashboard/webhooks). Klaviyo signs deliveries with `Klaviyo-Signature`; Unitpost signs each body with a `whsec_` secret in `svix-id`, `svix-timestamp` and `svix-signature`, so update your handler.

### 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 campaign from Unitpost to a small segment first, then to everyone. Turn each Klaviyo flow off once its replacement runs here, then delete the Klaviyo key.

## Same ideas, new names

| Klaviyo | Unitpost | Note |
| --- | --- | --- |
| Profile | Contact |  |
| List | Segment (static) |  |
| Segment | Segment (dynamic) | Members come over; rebuild the filter. |
| Custom property | Contact field |  |
| Never subscribed | Unsubscribed from marketing | Transactional email still reaches them. |
| Suppressed profile | Suppression |  |
| Campaign | Campaign |  |
| Flow | Automations | In early access. |
| Metric (event) | No launched equivalent | Send from your code when the event happens. |
| `{{ first_name }}` | `{{first_name}}` |  |
| `{% unsubscribe %}` | A link to `{{unsubscribe_url}}` |  |

## API calls

| Klaviyo API | Unitpost API | Note |
| --- | --- | --- |
| POST /api/profile-import | POST /api/v1/contacts | Klaviyo's call upserts; here an existing contact answers 409 and you update it with PATCH. |
| GET /api/profiles | GET /api/v1/contacts |  |
| PATCH /api/profiles/{id} | PATCH /api/v1/contacts/{id or email} |  |
| GET /api/lists | GET /api/v1/segments |  |
| POST /api/lists/{id}/relationships/profiles | POST /api/v1/segments/{id}/contacts |  |
| POST /api/profile-subscription-bulk-create-jobs | POST /api/v1/contacts/{id}/sms-consent | Email consent is the contact's `unsubscribed` flag; SMS consent is recorded per contact. |
| POST /api/profile-suppression-bulk-create-jobs | POST /api/v1/suppressions |  |
| POST /api/events | No launched equivalent | Events start Klaviyo flows. Send the email from the code that fires the event, with POST /api/v1/email. |
| POST /api/campaigns | POST /api/v1/email/campaigns |  |
| POST /api/webhooks | POST /api/v1/webhooks |  |
| `Authorization: Klaviyo-API-Key …` plus a `revision` header | `Authorization: Bearer <api key>` |  |

## Webhook events

| Klaviyo event | Unitpost event | Note |
| --- | --- | --- |
| event:klaviyo.received_email | email.delivered |  |
| event:klaviyo.opened_email | email.opened |  |
| event:klaviyo.clicked_email | email.clicked |  |
| event:klaviyo.bounced_email | email.bounced |  |
| event:klaviyo.marked_email_as_spam | email.complained |  |
| event:klaviyo.dropped_email | email.suppressed |  |
| event:klaviyo.subscribed_to_email_marketing, event:klaviyo.unsubscribed_from_email_marketing | contact.updated |  |
| event:klaviyo.sent_sms | sms.sent |  |
| event:klaviyo.received_sms | sms.received |  |
| event:klaviyo.failed_to_deliver_sms | sms.failed |  |
| Every topic | Add the endpoint by hand | Klaviyo's API truncates webhook URLs, so the importer lists your webhooks instead of copying them. |

## Before you cancel Klaviyo

- Export the profiles with no email address if you need them; the importer leaves them out.
- Export your suppressed profiles (Audience → Profiles → View suppressed profiles) and import them as suppressions, so complaints shown as unsubscribes stay blocked.
- Write down each flow with its trigger, filters and timing.
- Note the filters of the segments you'll rebuild; only their members came over.
- Delete Klaviyo's DNS records once no mail goes through Klaviyo. Keep `_dmarc` and Unitpost's records.
- Delete the Klaviyo API key you used for the import.

Facts about Klaviyo checked 2026-11-06.

## FAQ

### Why are some profiles missing?

Contacts here need an email address, so Klaviyo profiles with only a phone number, or with no identifier, are left out. The preview counts them. Export them from Klaviyo if you need them.

### Why did customers who never subscribed come over unsubscribed?

Klaviyo can email profiles that never subscribed, such as checkout-only customers, on implied consent, and counts them on your bill. The importer brings them over unsubscribed from marketing: transactional email still reaches them, and campaigns go only to people who subscribed.

### Do my SMS subscribers come over?

Yes, when the profile has an email address. An SMS opt-in carries over with the date Klaviyo recorded for it, and an opt-out always carries over. Texting from Unitpost needs a paid plan.

### Do my flows come over?

No. The preview lists them with their triggers. Rebuild them in Automations, which are in early access, or send from your code when the event happens.

## Related

- [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.
- [Migrate from Postmark to Unitpost](https://www.unitpost.com/migration/postmark): Leave Postmark in order: import every server's suppressions, templates and webhooks, keep broadcast unsubscribes off receipts, re-verify DNS, swap the client.
