Migrate from Postmark to emailr_
Move from Postmark to emailr_: map servers, message streams, templates, tags and metadata, translate webhooks and import suppressions step by step.
Postmark is built around servers and message streams. emailr_ is built around domain workspaces, with transactional sends going through the API and marketing sends through Broadcasts. This guide shows how each piece maps across.
The code change is mostly renaming fields: Postmark uses PascalCase (HtmlBody, TextBody, MessageStream), emailr_ uses lowercase fields in the same spirit.
The code change
A typical Postmark send call next to its emailr_ equivalent.
import { ServerClient } from 'postmark'
const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN!)
await client.sendEmail({
From: 'hello@mail.acme.com',
To: 'user@example.com',
Subject: 'Your receipt',
HtmlBody: '<p>Thanks for your order.</p>',
Tag: 'receipts',
Metadata: { order_id: '123' },
MessageStream: 'outbound',
})import { Emailr } from 'emailr'
const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! })
await emailr.emails.send({
from: 'hello@mail.acme.com',
to: 'user@example.com',
subject: 'Your receipt',
html: '<p>Thanks for your order.</p>',
tags: { category: 'receipts', order_id: '123' },
})Postmark concepts in emailr_
Use this table to find where each feature you rely on lives in emailr_.
| Postmark | emailr_ | Notes |
|---|---|---|
| Server API token | API key (et_live_ prefix) | |
| Servers | Domain workspaces | Each key is scoped to a workspace. |
| Transactional message stream | emails.send() API | |
| Broadcast message stream | Broadcasts | |
Templates (Mustachio, TemplateModel) | Templates (template_id + template_data) | |
Tag + Metadata | tags object | |
| Suppressions per stream | Contacts with subscribed: false, plus Topics | |
| Inbound stream | Inboxes and email.received webhooks |
How to migrate from Postmark
- 01
Create an API key
Create a key under API keys in the dashboard. Keys start with
et_live_and are scoped to one domain workspace. Store it asEMAILR_API_KEYin your secret manager or environment, never in client-side code. - 02
Add and verify your sending domain
In the dashboard, open Domains and add the domain you send from. A subdomain such as
mail.yourcompany.comkeeps transactional reputation separate from your corporate mailbox.Emailr generates the DNS records: a DKIM TXT record on the
emailr._domainkeyselector, an SPF TXT record, and an MX + TXT pair on thesendsubdomain used as the MAIL FROM / Return-Path. A DMARC record is optional but recommended.Postmark's DKIM record uses a dated
…pm._domainkeyselector and apm-bouncesReturn-Path CNAME. Neither conflicts with Emailr's records.Emailr's DKIM selector and
sendsubdomain don't collide with other providers' records, so you can add them before cutting over and send from both providers during the migration. A domain can only have one SPF record: if one already exists, merge Emailr'sinclude:into it instead of adding a second record. - 03
Swap the SDK
Install the SDK and rename fields:
From→from,To→to,HtmlBody→html,TextBody→text,ReplyTo→reply_to_email. DropMessageStream: API sends are transactional by default.TagandMetadatamerge intotags.Postmark attachments (
Name,Content,ContentType) becomefilename,contentandcontentType.Terminalbashnpm install emailr - 04
Move templates
Replace
sendEmailWithTemplate({ TemplateAlias, TemplateModel })withemails.send({ template_id, template_data }). Template-level subject, from and reply-to are used when you don't override them.Emailr templates substitute flat
{{variable}}tokens fromtemplate_data. There are no loops or conditionals, so pre-render lists and optional blocks in your code and pass the result as a variable. - 05
Move broadcast streams and suppressions
Newsletters sent on a Postmark broadcast stream move to emailr_ Broadcasts. Import each stream's suppressions as contacts with
subscribed: false, or use Topics if you need per-category opt-outs. - 06
Recreate your event webhooks
Postmark configures webhooks per server and stream. In emailr_ create one
transactionalwebhook per workspace and branch on theeventfield.Create a webhook of type
transactional(delivery and engagement events),domain(verification status) orreceiving(inbound email). Each webhook has its own signing secret. Every request carries anX-Emailr-Signatureheader: a hex-encoded HMAC-SHA256 of the raw request body. Verify it before trusting the payload.verify-webhook.tstsimport { createHmac, timingSafeEqual } from 'node:crypto' // rawBody must be the exact request body string, before JSON.parse export function isValidEmailrWebhook(rawBody: string, signature: string | null) { if (!signature) return false const expected = createHmac('sha256', process.env.EMAILR_WEBHOOK_SECRET!) .update(rawBody) .digest('hex') const a = Buffer.from(expected) const b = Buffer.from(signature) return a.length === b.length && timingSafeEqual(a, b) } // const ok = isValidEmailrWebhook(body, req.headers.get('x-emailr-signature')) // const { event, data } = JSON.parse(body) // e.g. event === 'email.bounced'
Webhook event mapping
emailr_ webhooks post one JSON event per request with an event name and a data object. Translate event names in your handler with this table.
| Postmark event | emailr_ event |
|---|---|
| Delivery | email.delivered |
| Bounce (hard) | email.bounced |
| SpamComplaint | email.complained |
| Open | email.opened |
| Click | email.clicked |
Things to watch for
- Postmark accepts a single
Tagstring; emailr_ tags are key/value pairs, so pick a key such ascategory. - Postmark's batch endpoint sends up to 500 messages per call. With emailr_, send individually or use Broadcasts for audience sends.
- Mustachio sections (
{{#items}}) aren't supported in emailr_ templates; render repeated blocks in code.
Frequently asked questions
What replaces Postmark servers in emailr_?
Domain workspaces. A workspace groups a primary domain, its aliases, templates, contacts and inboxes, and new API keys are scoped to a single workspace.
How do Postmark message streams map to emailr_?
Transactional streams map to the emails API. Broadcast streams map to emailr_ Broadcasts, which handle unsubscribe links and skip unsubscribed contacts automatically.
Can I move my Postmark templates?
Yes, recreate them as emailr_ templates. Variables written as {{name}} carry over directly; Mustachio sections and partials need to be pre-rendered in code.
Does emailr_ support inbound email like Postmark's inbound stream?
Yes. Create an inbox for the address. Incoming messages are threaded and you can receive email.received webhooks for each one.
Keep going
Documentation
Related guides
- Migrate from SendGridMove from SendGrid to emailr_: map dynamic templates, categories, suppressions and Event Webhook events, swap the SDK and cut over with zero downtime.
- Migrate from MailgunSwitch from Mailgun to emailr_: replace mailgun.js, map tags, variables, templates and webhook events, move suppressions and cut over without downtime.
- Migrate from Mailchimp TransactionalMove from Mailchimp Transactional (Mandrill) to emailr_: map templates, merge vars, tags, metadata, rejection lists and webhooks without downtime.
Ready to leave Postmark?
Create a free account, verify your domain and send your first email through emailr_ in minutes. No credit card required.