---
title: "Migrate from Brevo to Unitpost"
description: "Leave Brevo in order: import contacts, lists, blocked contacts and templates, rewrite params variables, re-verify DNS, re-record SMS consent, swap the call."
url: https://www.unitpost.com/migration/brevo
section: Migration
updated: 2026-11-02
---
# Migrate from Brevo to Unitpost

To leave Brevo, paste a Brevo API key into Unitpost's Migration Assistant. It imports every contact with its attributes and email blocklist state, lists as static segments, transactional blocked contacts as suppressions, transactional templates as drafts, both webhook types switched off, and domains where Brevo's API exposes them. Segments, SMS consent, automations, campaign history, CRM and landing pages stay at Brevo, and `{{ params.x }}` placeholders need rewriting.

**Easy migration.** Contacts with their email blocklist state, transactional blocked contacts and transactional templates all import; what you rebuild sits outside those lists (segments, SMS consent, automations).

## What comes with you

| Data | Result | What happens |
| --- | --- | --- |
| Contacts | Imports | Every contact imports with `FIRSTNAME` and `LASTNAME` as its name and other attributes as custom fields; address objects and multiple-choice values are flattened into separate fields. |
| Unsubscribes and consent | Imports | A contact Brevo has blocklisted for email (`emailBlacklisted`) imports unsubscribed, which stops marketing here. Transactional contacts that unsubscribed import as unsubscribe suppressions. |
| Suppressions (bounces and complaints) | Imports | Transactional blocked contacts import with their reason: hard bounces, spam complaints and admin blocks. Admin blocks stop every send here, transactional included. |
| Lists and segments | Partly imports | Lists become static segments with their members, and list folders are flattened. Brevo segments are not read; rebuild them as dynamic segments. |
| Templates | Partly imports | Transactional templates import subject and HTML as drafts in code mode; text versions are not read. `{{ params.x }}` and `{{ contact.X }}` stay as written, and the preview warns about them. |
| Sending domains | Partly imports | Domains and their sender addresses are recreated as verifying with new DKIM keys when Brevo's domains API answers for your account; when it does not, the preview says so and you add them by hand. |
| Webhooks | Partly imports | Transactional and marketing webhooks import switched off with a new signing secret. `request`, `uniqueOpened` and `listAddition` are not mapped and are listed in the preview. |
| Topics | Rebuild | Brevo has no topics. Create them here if you want per-category opt-outs on campaigns. |
| API keys | Rebuild | API keys and SMTP keys stay at Brevo. Create Unitpost API keys instead. |
| SMS contacts and consent | Rebuild | The `SMS` attribute arrives as a text custom field, not as the contact's SMS number, and `smsBlacklisted` is not read. Re-record SMS consent before texting anyone. |
| Email campaigns | Rebuild | Not read. Recreate drafts and scheduled campaigns as Unitpost campaigns on a segment. |
| Send history and analytics | Stays behind | Email logs, campaign reports and statistics stay at Brevo. |
| Automations | Stays behind | Brevo automations stay; Unitpost automations are not launched yet. |
| CRM, landing pages, chat and WhatsApp | Stays behind | Unitpost has no equivalent; keep Brevo for them if you use them. |

## Before you start

- A Brevo API key (`xkeysib-…`) from Settings → SMTP & API. Keys created with the MCP option work too.
- If Brevo blocks unknown IPs for API keys (Security → Authorised IPs), choose Deactivate for API keys before connecting, or Brevo rejects even a valid key.
- A Unitpost API key with `emails:send`, 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.
- If you text contacts: SMS on Unitpost is on paid plans only, and marketing texts need consent recorded per contact.

## The cut-over plan

### 1. Run the Migration Assistant

Paste the key in [Settings → Migration](/dashboard/settings/migration). Suppressions are written before contacts, so blocked and blocklisted addresses land unsubscribed. Check the domains line in the preview: if Brevo's domains API is not enabled for your account, add the domains by hand in step 2.

### 2. Verify your domains next to Brevo

Open [Domains](/dashboard/email/domains) (add any the importer could not read) and publish the records shown: MX and SPF on `unitpost.<your domain>` and a DKIM TXT under `_domainkey`. They do not replace Brevo's records, so both providers stay authenticated. Keep your single `_dmarc` record.

### 3. Rewrite template variables and publish

`{{ params.order_id }}` becomes `{{order_id}}`, filled from `template.variables`. `{{ contact.FIRSTNAME }}` and `{{ contact.LASTNAME }}` become `{{first_name}}` and `{{last_name}}`, which fill from the recipient's contact. Set receipts to `transactional` (imports land as marketing, which adds a footer) and publish with `PATCH /api/v1/email/templates/{id}`.

### 4. Switch the API call

`sender` becomes a `from` string, `to` takes addresses, `htmlContent` becomes `html`, and `templateId` with `params` becomes `template: { id, variables }`. The `api-key` header becomes `Authorization: Bearer`, and sends accept an `Idempotency-Key`.

```ts
// Before: Brevo
await fetch("https://api.brevo.com/v3/smtp/email", {
  method: "POST",
  headers: { "api-key": process.env.BREVO_API_KEY, "content-type": "application/json" },
  body: JSON.stringify({
    sender: { name: "Acme", email: "hi@acme.com" },
    to: [{ email: "user@example.com" }],
    templateId: 8,
    params: { order_id: "A-1009" },
  }),
});

// After: Unitpost
import { Unitpost } from "unitpost";
const unitpost = new Unitpost(process.env.UNITPOST_API_KEY);
const { data, error } = await unitpost.email.send({
  from: "Acme <hi@acme.com>",
  to: "user@example.com",
  template: { id: "tmpl_123", variables: { order_id: "A-1009" } },
});
```

### 5. Re-key and enable webhooks

Brevo protects webhooks with credentials in the URL, a bearer token or an IP allowlist; Unitpost signs each body with a `whsec_` secret in the svix headers (`svix-id`, `svix-timestamp`, `svix-signature`). Rotate the secret in [Webhooks](/dashboard/webhooks) to see it, add verification, add `request` events by hand if you used them, then set `status` to `enabled`.

### 6. Re-record SMS consent

Brevo's SMS opt-outs do not import. Before any marketing text, record each contact's consent where they gave it (`POST /api/v1/contacts/{id}/sms-consent`) or ask them to confirm with `POST /api/v1/contacts/{id}/sms-opt-in-request`. A phone number on a contact is not consent.

### 7. Send tests

Send each template with `POST /api/v1/email/templates/{id}/test-send`, and fire `POST /api/v1/webhooks/{id}/test` at each endpoint.

### 8. Move traffic in stages

Move transactional mail first, watching `email.bounced` and `email.complained`, then campaigns. Brevo stays authenticated, so rolling back is a config change.

### 9. Cancel Brevo last

Finish the checks below, then downgrade or close the account.

## Same ideas, new names

| Brevo | Unitpost | Note |
| --- | --- | --- |
| Contact list | Segment (static) |  |
| Segment | Segment (dynamic) | Rebuilt by hand. |
| List folder | No equivalent | Flattened on import. |
| Contact attribute | Contact field |  |
| Email blocklisted (`emailBlacklisted`) | Unsubscribed contact |  |
| Transactional blocked contact | Suppression |  |
| `params` | Template `variables` |  |
| Email campaign | Campaign |  |
| Automation | Automations | Not launched yet. |

## API calls

| Brevo API | Unitpost API | Note |
| --- | --- | --- |
| POST /v3/smtp/email | POST /api/v1/email |  |
| `templateId` + `params` | `template: { id, variables }` |  |
| `api-key` header | `Authorization: Bearer <api key>` |  |
| POST /v3/contacts | POST /api/v1/contacts |  |
| PUT /v3/contacts/{identifier} | PATCH /api/v1/contacts/{id or email} |  |
| POST /v3/contacts/import | POST /api/v1/contacts/imports | Up to 5,000 rows per request. |
| GET /v3/contacts/segments | GET /api/v1/segments |  |
| GET /v3/smtp/blockedContacts | GET /api/v1/suppressions |  |
| POST /v3/emailCampaigns | POST /api/v1/email/campaigns |  |
| POST /v3/webhooks | POST /api/v1/webhooks |  |

## SMTP settings

| Brevo SMTP | Unitpost SMTP | Note |
| --- | --- | --- |
| smtp-relay.brevo.com | smtp.unitpost.com |  |
| Login: the SMTP login from SMTP & API settings | Username `unitpost` |  |
| Password: an SMTP key (not the API key) | Password: a Unitpost API key with `emails:send` |  |
| 587, 2525 (encryption optional) | 587, 2587, 25 (STARTTLS) | TLS 1.2 or later is required before login. |
| 465 (SSL or TLS) | 465, 2465 (implicit TLS) |  |

## Webhook events

| Brevo event | Unitpost event | Note |
| --- | --- | --- |
| sent | email.sent |  |
| request | email.sent | Brevo's other name for `sent`; not mapped by the importer. |
| delivered | email.delivered |  |
| softBounce, deferred | email.delivery_delayed |  |
| hardBounce | email.bounced |  |
| spam | email.complained |  |
| opened | email.opened |  |
| uniqueOpened | email.opened | Not mapped by the importer. |
| click | email.clicked |  |
| blocked, invalid | email.failed | A send our suppression list stops is `email.suppressed`. |
| unsubscribed, contactUpdated | contact.updated |  |
| contactDeleted | contact.deleted |  |
| listAddition | segment.joined | Not mapped; bulk imports do not fire it. |

## Before you cancel Brevo

- Export contacts and segments as files, including SMS numbers and SMS opt-outs, which do not import.
- Export the campaign reports and email logs you want to keep; none of them move.
- Pull the transactional blocked contacts one last time and add new ones here.
- Delete Brevo's DKIM and verification records once no mail goes through Brevo. Keep `_dmarc` and Unitpost's records.
- Turn the IP allowlist back on if you switched it off for the migration, then revoke the API key you used.

Facts about Brevo checked 2026-11-02.

## FAQ

### Brevo's free plan sends more. Why switch?

It does: 300 emails a day, about 9,000 a month, against Unitpost's 5,000 a month. If free volume decides it, keep Brevo. Unitpost is for teams whose main interface is code: one REST API, SDKs, SMTP and an MCP server on the same send rules.

### Do my Brevo automations come over?

No. They stay in Brevo, and Unitpost automations are not launched yet, so trigger those emails from your code until they are.

### Will my SMS opt-outs come with me?

No. Only email consent imports. Phone numbers arrive as a text field, and you record SMS consent per contact before any marketing text.

### Brevo rejected the connection with a valid key. Why?

Brevo's authorised IPs setting refuses API calls from unlisted addresses even with a valid key. Choose Deactivate for API keys under Security → Authorised IPs, connect, then turn it back on.

### Can Unitpost replace Brevo entirely?

Only for email and SMS. Unitpost has no CRM, landing pages, live chat or WhatsApp, so teams using Brevo as their whole marketing suite keep parts of it.

## Related

- [Migrate from Loops to Unitpost](https://www.unitpost.com/migration/loops): Leave Loops in order: export the audience CSV, turn subscribed into an unsubscribed column, copy suppressions, import lists as topics, rebuild emails.
- [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.
