---
title: "Migrate from Postmark to Unitpost"
description: "Leave Postmark in order: import every server's suppressions, templates and webhooks, keep broadcast unsubscribes off receipts, re-verify DNS, swap the client."
url: https://www.unitpost.com/migration/postmark
section: Migration
updated: 2026-11-02
---
# Migrate from Postmark to Unitpost

To leave Postmark, paste an Account API token into Unitpost's Migration Assistant. It reads every server: hard bounces, spam complaints and manual suppressions from each message stream, templates as drafts, webhooks switched off, and your domains with new DKIM keys. Postmark scopes suppressions to a stream and Unitpost to the workspace, so pull broadcast unsubscribes out before they block receipts. Layouts, sender signatures, message history and API tokens do not move.

**Medium migration.** Postmark keeps no contacts to lose, and suppressions, templates, domains and webhooks import; the hand work is taking broadcast unsubscribes out of the suppression list and merging layouts into templates.

## What comes with you

| Data | Result | What happens |
| --- | --- | --- |
| Suppressions (bounces and complaints) | Imports | Hard bounces and spam complaints from every stream on every server import with their reason. Complaint rows cannot be removed here, as at Postmark. |
| Unsubscribes and consent | Partly imports | Postmark records unsubscribes and manual blocks as `ManualSuppression` on one stream. They import as workspace suppressions that stop every send, so a broadcast unsubscriber would stop getting password resets until step 2. |
| Templates | Partly imports | Subject, HTML and text import as draft code-mode templates in the marketing category. Layouts arrive as separate templates and are not merged into the templates that use them; same-named templates from two servers get a ` (2)` suffix. |
| Sending domains | Partly imports | With an Account API token, each domain is recreated as verifying with a new DKIM key. A server token cannot read domains, and sender signatures (single verified addresses) are not read. |
| Webhooks | Partly imports | Each server's webhooks import with their enabled triggers, switched off and with a new signing secret. HTTP auth credentials and custom headers set on them are not copied. |
| Contacts | Rebuild | Postmark stores no contacts or lists, so nothing imports. If you start campaigns here, import contacts from your own database with `POST /api/v1/contacts/imports`. |
| Segments or lists | Rebuild | Postmark has no lists or segments. Create segments here when you start campaigns. |
| Topics | Rebuild | Postmark has no topics. Create them here if you want per-category opt-outs on campaigns. |
| API keys and tokens | Rebuild | Server and account tokens stay at Postmark. Create Unitpost API keys instead. |
| Automations and broadcasts | Rebuild | Postmark has no automations to move. Broadcasts sent from your code become campaigns here, which need contacts and a segment. |
| Message streams | Rebuild | Unitpost has no streams. A template's `category` (`transactional` or `marketing`) decides footers and opt-outs, broadcasts become campaigns, and the inbound stream becomes receiving on a verified domain. |
| Send history and analytics | Stays behind | Message content and activity stay at Postmark, kept 45 days by default (7 to 365 with its Retention Add-on). |

## Before you start

- An Account API token (Account → API Tokens). It reads domains and, through each server's own token, every server's templates, suppressions and webhooks. A server token reads one server and no domains.
- A list of which message streams are broadcast streams, for step 2.
- A Unitpost API key with `emails:send` for your app, created in [API keys](/dashboard/api-keys).
- A second Unitpost key for the migration steps, with `contacts:write`, `domains:write`, `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. Run the Migration Assistant

Paste the token in [Settings → Migration](/dashboard/settings/migration). The preview lists suppressions with their Postmark reason (`bounce`, `complaint`, `manual`). Stop before you accept if you have broadcast streams.

### 2. Keep broadcast unsubscribes off receipts

Postmark scopes a suppression to one stream; here it covers the workspace and every send. Pull each broadcast stream's list with `GET /message-streams/{stream_id}/suppressions/dump`, exclude its `ManualSuppression` addresses in the preview, then import them as unsubscribed contacts (`POST /api/v1/contacts/imports` with `unsubscribed: true`). An unsubscribed contact gets no marketing but still gets receipts and resets.

> Already accepted? Remove those rows with `DELETE /api/v1/suppressions/{email}` first, then import the contacts.

### 3. Verify your domains next to Postmark

Open [Domains](/dashboard/email/domains) and publish the records shown: MX and SPF on `unitpost.<your domain>` and a DKIM TXT under `_domainkey`. Postmark's return path stays on its own CNAME (`pm-bounces` by default), so both stay verified. Unitpost has no single-address verification: an address from a sender signature needs its domain verified here.

### 4. Merge layouts and publish templates

Paste each layout's wrapper around the template body it served, and delete the imported layout drafts. Plain `{{name}}` placeholders work; Mustachio sections (`{{#...}}`) do not, because Unitpost templates have no loops or conditionals. Postmark mail is usually transactional, so set `category` to `transactional` (imports land as marketing, which adds an unsubscribe footer) and publish with `PATCH /api/v1/email/templates/{id}`.

### 5. Switch the client

Fields move from PascalCase to snake_case (`HtmlBody` becomes `html`), `TemplateModel` becomes `template.variables`, and the stream disappears: the template's category does that job. `first_name`, `last_name` and `email` are reserved variable names here.

```ts
// Before: Postmark
import { ServerClient } from "postmark";
const postmark = new ServerClient(process.env.POSTMARK_SERVER_TOKEN);
await postmark.sendEmailWithTemplate({
  From: "hi@acme.com",
  To: "user@example.com",
  TemplateAlias: "password-reset",
  TemplateModel: { reset_url: resetUrl },
  MessageStream: "outbound",
});

// 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: { reset_url: resetUrl } },
});
```

### 6. Add signature checks to webhook receivers

Postmark protects webhooks with HTTP auth or headers you set; Unitpost signs each delivery with a `whsec_` secret in the svix format (`svix-id`, `svix-timestamp`, `svix-signature`). Rotate the secret in [Webhooks](/dashboard/webhooks) to see it, verify with the SDK's `verifyWebhook`, then set `status` to `enabled`. A switched-off endpoint does not queue events.

### 7. Move inbound mail

If you use an inbound stream, turn receiving on for the verified domain (`PATCH /api/v1/email/domains/{id}` with `capabilities.receiving` set to `enabled`), subscribe an endpoint to `email.received`, then switch the MX record.

### 8. Send tests

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

### 9. Move traffic in stages

Move the mail where a delay costs least first, and password resets and sign-in codes last, watching `email.bounced` and `email.delivery_delayed` as you go. Postmark stays verified, so rolling back is a config change.

### 10. Cancel Postmark last

Finish the checks below, then delete the servers and close the account.

## Same ideas, new names

| Postmark | Unitpost | Note |
| --- | --- | --- |
| Server | Workspace | The importer merges every server into one workspace. |
| Transactional stream (`outbound`) | Template category `transactional` |  |
| Broadcast stream | Campaign | Marketing category, unsubscribe footer added. |
| Inbound stream | Receiving on a domain |  |
| Server API token | API key with scopes |  |
| Sender signature | Verified domain | No single-address verification. |
| Template layout | No equivalent | Merge it into each template. |
| Suppression (per stream) | Suppression (per workspace) |  |

## API calls

| Postmark API | Unitpost API | Note |
| --- | --- | --- |
| POST /email | POST /api/v1/email | PascalCase fields become snake_case. |
| POST /email/withTemplate | POST /api/v1/email with `template: { id, variables }` |  |
| POST /email/batch | POST /api/v1/email/batch | 500 messages per call there, 100 here. |
| `X-Postmark-Server-Token` header | `Authorization: Bearer <api key>` |  |
| GET /templates | GET /api/v1/email/templates |  |
| GET /message-streams/{stream_id}/suppressions/dump | GET /api/v1/suppressions |  |
| POST /webhooks | POST /api/v1/webhooks |  |
| GET /domains | GET /api/v1/email/domains |  |

## SMTP settings

| Postmark SMTP | Unitpost SMTP | Note |
| --- | --- | --- |
| smtp.postmarkapp.com | smtp.unitpost.com |  |
| smtp-broadcasts.postmarkapp.com | No SMTP equivalent | Send broadcasts as campaigns. |
| Username: server API token or SMTP token access key | Username `unitpost` |  |
| Password: server API token or SMTP token secret key | Password: Unitpost API key with `emails:send` |  |
| 25, 2525, 587 (STARTTLS) | 587, 2587, 25 (STARTTLS), or 465, 2465 (implicit TLS) |  |
| `X-PM-Message-Stream` header | No equivalent | One SMTP host; broadcasts go through campaigns. |

## Webhook events

| Postmark event | Unitpost event | Note |
| --- | --- | --- |
| Delivery | email.delivered |  |
| Bounce | email.bounced |  |
| SpamComplaint | email.complained |  |
| Open | email.opened |  |
| Click | email.clicked |  |
| SubscriptionChange | contact.updated | The importer's mapping. |
| Inbound webhook | email.received | Not imported; needs receiving on. |
| No Postmark event | email.sent, email.delivery_delayed, email.failed |  |

## Before you cancel Postmark

- Export the message history you need; Postmark keeps it 45 days by default and none of it moves.
- Pull each stream's suppression dump one last time and add new hard bounces and complaints here.
- Delete Postmark's DKIM record and its return-path CNAME once no mail goes through Postmark. Keep `_dmarc` and Unitpost's records.
- Move the MX record off Postmark's inbound address if you used an inbound stream.
- Revoke server and account tokens and delete webhooks on every server.

Facts about Postmark checked 2026-11-02.

## FAQ

### Is Unitpost as reliable as Postmark for password resets?

We do not claim that. Postmark's deliverability record is the best regarded in the category; Unitpost has the mechanisms (verified SPF, DKIM and DMARC, automatic bounce and complaint suppression) but no public inbox-placement record yet. If a late reset is expensive for you, move those last.

### Will my broadcast unsubscribes block password resets?

Yes, if you import them as they are, because a migrated suppression stops every send. Exclude them in the preview and import them as unsubscribed contacts (step 2), which stops only marketing.

### Does Unitpost separate transactional and marketing like message streams?

Inside one product. A marketing template always gets an unsubscribe footer and List-Unsubscribe headers and sends to one recipient per message; batch marketing goes through campaigns. Transactional templates get no footer.

### What happens to my Postmark layouts?

They import as separate draft templates, and the templates that used them arrive without the wrapper. Merge each layout into its templates before you publish.

## Related

- [Migrate from Mailgun to Unitpost](https://www.unitpost.com/migration/mailgun): Leave Mailgun in order: import lists and per-domain suppressions, check EU domains, re-verify DNS, rebuild routes as receiving, move to JSON and Bearer auth.
- [Migrate from Brevo to Unitpost](https://www.unitpost.com/migration/brevo): Leave Brevo in order: import contacts, lists, blocked contacts and templates, rewrite params variables, re-verify DNS, re-record SMS consent, swap the call.
- [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.
