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.

PostmarkUnitpost
Suppressions (bounces and complaints)Imports
Unsubscribes and consentPartly imports
ContactsRebuild
Send history and analyticsStays behind

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.

DataResultWhat happens
Suppressions (bounces and complaints)ImportsHard 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 consentPartly importsPostmark 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.
TemplatesPartly importsSubject, 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 domainsPartly importsWith 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.
WebhooksPartly importsEach 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.
ContactsRebuildPostmark 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 listsRebuildPostmark has no lists or segments. Create segments here when you start campaigns.
TopicsRebuildPostmark has no topics. Create them here if you want per-category opt-outs on campaigns.
API keys and tokensRebuildServer and account tokens stay at Postmark. Create Unitpost API keys instead.
Automations and broadcastsRebuildPostmark has no automations to move. Broadcasts sent from your code become campaigns here, which need contacts and a segment.
Message streamsRebuildUnitpost 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 analyticsStays behindMessage 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.
  • 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

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

  1. 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.

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

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

Name changes

Same ideas, new names

PostmarkUnitpostNote
ServerWorkspaceThe importer merges every server into one workspace.
Transactional stream (outbound)Template category transactional
Broadcast streamCampaignMarketing category, unsubscribe footer added.
Inbound streamReceiving on a domain
Server API tokenAPI key with scopes
Sender signatureVerified domainNo single-address verification.
Template layoutNo equivalentMerge it into each template.
Suppression (per stream)Suppression (per workspace)

API calls

Postmark APIUnitpost APINote
POST /emailPOST /api/v1/emailPascalCase fields become snake_case.
POST /email/withTemplatePOST /api/v1/email with template: { id, variables }
POST /email/batchPOST /api/v1/email/batch500 messages per call there, 100 here.
X-Postmark-Server-Token headerAuthorization: Bearer <api key>
GET /templatesGET /api/v1/email/templates
GET /message-streams/{stream_id}/suppressions/dumpGET /api/v1/suppressions
POST /webhooksPOST /api/v1/webhooks
GET /domainsGET /api/v1/email/domains

SMTP settings

Postmark SMTPUnitpost SMTPNote
smtp.postmarkapp.comsmtp.unitpost.com
smtp-broadcasts.postmarkapp.comNo SMTP equivalentSend broadcasts as campaigns.
Username: server API token or SMTP token access keyUsername unitpost
Password: server API token or SMTP token secret keyPassword: Unitpost API key with emails:send
25, 2525, 587 (STARTTLS)587, 2587, 25 (STARTTLS), or 465, 2465 (implicit TLS)
X-PM-Message-Stream headerNo equivalentOne SMTP host; broadcasts go through campaigns.

Webhook events

Postmark eventUnitpost eventNote
Deliveryemail.delivered
Bounceemail.bounced
SpamComplaintemail.complained
Openemail.opened
Clickemail.clicked
SubscriptionChangecontact.updatedThe importer's mapping.
Inbound webhookemail.receivedNot imported; needs receiving on.
No Postmark eventemail.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. 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?

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.

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.