Migrate from Postmark to Unitpost
Medium migrationPostmark 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
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.
| 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:sendfor your app, created in API keys. - A second Unitpost key for the migration steps, with
contacts:write,domains:write,suppressions:write,templates:writeandwebhooks:manage, saved asUNITPOST_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
Postmark keeps sending until the last step, so there is no gap in delivery.
Run the Migration Assistant
Paste the token in Settings → Migration. The preview lists suppressions with their Postmark reason (
bounce,complaint,manual). Stop before you accept if you have broadcast streams.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 itsManualSuppressionaddresses in the preview, then import them as unsubscribed contacts (POST /api/v1/contacts/importswithunsubscribed: 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.Verify your domains next to Postmark
Open 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-bouncesby default), so both stay verified. Unitpost has no single-address verification: an address from a sender signature needs its domain verified here.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 setcategorytotransactional(imports land as marketing, which adds an unsubscribe footer) and publish withPATCH /api/v1/email/templates/{id}.Switch the client
Fields move from PascalCase to snake_case (
HtmlBodybecomeshtml),TemplateModelbecomestemplate.variables, and the stream disappears: the template's category does that job.first_name,last_nameandemailare reserved variable names here.send.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 } }, });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 to see it, verify with the SDK'sverifyWebhook, then setstatustoenabled. A switched-off endpoint does not queue events.Move inbound mail
If you use an inbound stream, turn receiving on for the verified domain (
PATCH /api/v1/email/domains/{id}withcapabilities.receivingset toenabled), subscribe an endpoint toemail.received, then switch the MX record.Send tests
Send each template once with
POST /api/v1/email/templates/{id}/test-sendand firePOST /api/v1/webhooks/{id}/testat each endpoint.Move traffic in stages
Move the mail where a delay costs least first, and password resets and sign-in codes last, watching
email.bouncedandemail.delivery_delayedas you go. Postmark stays verified, so rolling back is a config change.Cancel Postmark last
Finish the checks below, then delete the servers and close the account.
Name changes
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
_dmarcand 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. Providers change their APIs and plans; check their docs before you rely on a detail.
Questions about leaving Postmark
Is Unitpost as reliable as Postmark for password resets?
Will my broadcast unsubscribes block password resets?
Does Unitpost separate transactional and marketing like message streams?
What happens to my Postmark layouts?
Leaving Postmark?
Preview the migration on the free tier
Verify a domain, paste a read-only key, and see what would come across before anything is written.