Guide
Migrate from Postmark
Move from Postmark to Emailit. Map servers, message streams and tokens, convert API fields, swap SMTP settings, and move webhooks, suppressions and templates.
This guide maps Postmark concepts, API calls, webhooks, suppressions and templates to their Emailit equivalents. Read Migrate to Emailit first for the overall order and how to run both providers in parallel.
Concepts
| Postmark | Emailit |
|---|---|
| Account | Account |
| Server | Workspace, or one workspace with several sending domains |
| Server API token | Sending Only API key, optionally restricted to one domain |
| Account API token | Full Access API key |
| Transactional message stream | The Email API and SMTP relay |
| Broadcast message stream | Campaigns to audiences, or the API with your own List-Unsubscribe header |
| Inbound message stream | Inbound email on a subdomain such as inbound.acme.com |
| Sender signatures and domains | Sending domains. Single-address sender signatures aren’t available. |
| Templates and layouts | Templates with an alias and versions. There are no layouts. |
| Webhooks per stream | Webhooks per workspace |
| Suppressions per stream | One suppression list per workspace |
| Activity | Email APIEmails and Email APILogs |
Tag and Metadata |
meta |
To keep transactional and marketing reputation apart, send them from different domains or subdomains, such as mail.acme.com for receipts and news.acme.com for newsletters.
Update your API calls
Postmark’s POST /email with an X-Postmark-Server-Token becomes POST /v2/emails with a bearer token. Field names change from PascalCase to snake_case:
curl https://api.postmarkapp.com/email \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
-d '{
"From": "Acme <hello@acme.com>",
"To": "ada@example.com",
"Subject": "Your receipt",
"TextBody": "Thanks for your order.",
"HtmlBody": "<p>Thanks for your order.</p>",
"MessageStream": "outbound"
}'curl https://api.emailit.com/v2/emails \
-H "Authorization: Bearer $EMAILIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": "ada@example.com",
"subject": "Your receipt",
"text": "Thanks for your order.",
"html": "<p>Thanks for your order.</p>"
}'| Postmark | Emailit |
|---|---|
X-Postmark-Server-Token header |
Authorization: Bearer secret_… |
From |
from |
To, Cc, Bcc (comma-separated strings) |
to, cc, bcc as a string or an array of up to 50 each |
ReplyTo |
reply_to |
Subject |
subject |
HtmlBody, TextBody |
html, text |
Headers: [{ "Name": "…", "Value": "…" }] |
headers: { "Name": "Value" } |
Metadata, Tag |
meta, returned in webhook events |
TrackOpens, TrackLinks |
tracking: { "loads": true, "clicks": true } |
Attachments[] with Name, Content, ContentType, ContentID |
attachments[] with filename, content, content_type, content_id |
MessageStream |
Not needed |
POST /email/withTemplate with TemplateAlias or TemplateId and TemplateModel |
The same POST /v2/emails with template (an alias or ID) and variables |
Response with MessageID and ErrorCode: 0 |
200 with id (em_…), status: "accepted" and ids per recipient. Errors use HTTP status codes. |
Emailit has no batch endpoint. Send one request per message, each with up to 50 recipients, and add an Idempotency-Key header so retries are safe. See Send an email.
Switch SMTP settings
| Setting | Postmark | Emailit |
|---|---|---|
| Host | smtp.postmarkapp.com |
smtp.emailit.com |
| Port | 587, 2525 or 25 |
587 (STARTTLS), 465 (TLS), 2525, 2587 or 25 |
| Username | Your server API token | emailit |
| Password | Your server API token | Your Emailit API key |
Emailit doesn’t read X-PM-* headers such as X-PM-Message-Stream or X-PM-Tag. Remove them, and set tracking on the domain instead. See SMTP settings.
Map webhook events
| Postmark webhook | Emailit event |
|---|---|
| Delivery | email.delivered |
| Bounce, hard bounce types | email.bounced |
| Bounce, soft or transient types | email.attempted while Emailit retries, then email.bounced if every retry fails |
| Spam complaint | email.complained |
| Open | email.loaded |
| Click | email.clicked |
| Subscription change | email.unsubscribed for campaign email, and suppression.created or suppression.deleted for suppressions you add or remove with the API |
| Inbound | email.received, then fetch the content with GET /emails/{id} |
Emailit also sends email.accepted when the API accepts an email, which Postmark has no webhook for.
The request format changes:
- Postmark posts one record per request and names it in
RecordType. Emailit posts a JSON array of up to 100 events, with the name intypeand the email indata.object. - Use
data.object.id, theem_ID from the send response, instead ofMessageID. Yourmetavalues are indata.object.meta. - Postmark webhooks are usually protected with basic authentication credentials in the URL. Emailit signs every request instead: verify
X-Emailit-SignatureagainstX-Emailit-Timestampand yourwhsec_secret. See Request signature.
for (const event of req.body) {
const email = event.data.object;
if (event.type === 'email.bounced') markBounced(email.to, email.id);
if (event.type === 'email.complained') unsubscribe(email.to);
}Move suppressions
-
Export the suppressions of each Postmark message stream you send from, from the stream’s suppressions page or with the suppressions dump API. Include hard bounces, spam complaints and manual suppressions.
-
Build one CSV with the columns
email,type,reason:email,type,reason old-address@example.com,recipient,postmark hard bounce angry@example.com,recipient,postmark spam complaintUse the type
recipientfor addresses that must never receive email. It blocks API, SMTP and campaign sends. For people who only opted out of your broadcasts, use the typeunsubscribe, which stops campaigns but still lets transactional email through. -
In Email APISuppressions, select Import and upload the file. Each file can have up to 10,000 rows and can be at most 8 MB. Duplicates are skipped.
See Manage suppressions.
Move templates
Copy each template’s HTML from Postmark, including its layout, then import it in Email MarketingTemplates or create it with the Templates API. Emailit has no layouts, so merge the layout and the content into one template. Use the same alias you used in Postmark so your code changes stay small.
Postmark templates use Mustachio. Temple covers simple values and conditions:
| Postmark (Mustachio) | Emailit (Temple) |
|---|---|
{{name}} |
{{name}} |
{{company.name}} |
{{company.name}} |
{{#company}}…{{/company}} sections |
{{#if company}}…{{/if}}, with full paths such as {{company.name}} inside |
{{^name}}…{{/name}} inverted sections |
{{#if name}}{{else}}…{{/if}} |
{{#each items}}…{{/each}} |
Not supported. Render the list in your code and pass it as one variable. |
TemplateModel |
variables |
Temple never escapes HTML, so escape user input before you pass it. A missing value renders as an empty string unless you add a default, such as {{name|"there"}}. See Temple.
Change DNS
Add your domain in Email APIDomains and publish the Emailit records. They use their own names (emailit._domainkey, emailit.<domain>, and optionally go and inbound), so they don’t conflict with Postmark’s DKIM record or its pm-bounces return-path CNAME. Keep your DMARC record. After the cutover, remove the Postmark records. See DNS records.
If you process inbound mail with Postmark, move it to an Emailit inbound subdomain and update the addresses your app gives out. See Set up inbound.
Next steps
- Go-live checklist
- Set up webhooks
- Priority migration: let Emailit engineers do the move with you