Migration guide
Migrate from Postmark to Mail.Land
Postmark and Mail.Land both take a JSON send call, so most fields map one to one. This guide lists each common field, the settings to change for SMTP, and the differences that need a code change.
# Before (Postmark)
SMTP_HOST=smtp.postmarkapp.com # broadcasts: smtp-broadcasts.postmarkapp.com
SMTP_PORT=587 # STARTTLS
SMTP_USER=<Server API token, or SMTP token access key>
SMTP_PASS=<Server API token, or SMTP token secret key>
# After (Mail.Land)
SMTP_HOST=smtp.mail.land
SMTP_PORT=587 # STARTTLS
SMTP_USER=apikey # any value; Mail.Land ignores it
SMTP_PASS=ml_pet_... # transactional; use an ml_pem_ key for broadcastsPostmark concepts in Mail.Land
| Postmark | Mail.Land |
|---|---|
| Server and Server API token | Workspace and API keys (credentials), each limited to the API or SMTP, to domains and to IP addresses |
| Message streams (transactional, broadcast) | The key's stream: transactional (ml_pet_) or marketing (ml_pem_) |
| Sender signatures and domains | Verified domain (SPF, DKIM, DMARC and return path) |
| Templates and layouts (Mustachio) | Templates with {{name}} placeholders |
| Suppressions per message stream | Suppressions with a type and a scope |
| Webhooks | Webhooks signed with HMAC-SHA256, a secret per webhook |
| Inbound stream | Inbound routes |
Create the right API keys
In Postmark the MessageStream field or header picks the stream. In Mail.Land the key does: its prefix tells you its mode and its stream. Create one key per application and stream.
| Prefix | What it is for |
|---|---|
ml_pet_ | Production, transactional: receipts, password resets, alerts. Up to 5 recipients per request. |
ml_pem_ | Production, marketing: newsletters and bulk sends with send-batch, up to 10,000 recipients per call. |
ml_set_ | Sandbox, transactional: with sandboxBehavior LogOnly (the default), requests are accepted and logged but never delivered. Use it for your integration tests. |
Each key can be limited to the API, to SMTP or both, to selected domains and to known IP addresses. A key is shown once, and Mail.Land stores only a hash of it.
Switch SMTP: change the host and the credentials
Postmark uses a Server API token or an SMTP token, and a header to pick the stream. Mail.Land uses your API key as the password, ignores the username, and takes the stream from the key.
| Setting | Postmark | Mail.Land |
|---|---|---|
| Host | smtp.postmarkapp.com, or smtp-broadcasts.postmarkapp.com for broadcasts | smtp.mail.land |
| Port | 25, 587 or 2525, with STARTTLS | 587 with STARTTLS |
| Username | Server API token, or SMTP token access key | Any value, for example apikey |
| Password | Server API token, or SMTP token secret key | A Mail.Land API key with SMTP enabled |
| Stream | X-PM-Message-Stream header | The key: ml_pet_ for transactional, ml_pem_ for marketing |
Mail.Land accepts sign-in only after STARTTLS. Mail.Land does not act on the X-PM-Message-Stream header: the key decides the stream. Messages can be up to 15 MiB.
Switch the API call
Both APIs take JSON. Postmark uses PascalCase names and the X-Postmark-Server-Token header; Mail.Land uses camelCase names and the X-API-Key header. Here is the same message in both, and a template send.
curl -X POST https://api.postmarkapp.com/email \
-H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"From": "Your Shop <[email protected]>",
"To": "[email protected]",
"ReplyTo": "[email protected]",
"Subject": "Order 12345 has shipped",
"TextBody": "Your order is on its way.",
"HtmlBody": "<p>Your order is on its way.</p>",
"Tag": "order-shipped",
"MessageStream": "outbound"
}'curl -X POST https://api.mail.land/v1/transactional/send \
-H "X-API-Key: $MAILLAND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": { "email": "[email protected]", "name": "Your Shop" },
"to": [{
"email": "[email protected]", "name": "Jane",
"variables": { "orderNumber": "12345" }
}],
"replyTo": "[email protected]",
"subject": "Order {{orderNumber}} has shipped",
"textContent": "Your order {{orderNumber}} is on its way.",
"htmlContent": "<p>Your order {{orderNumber}} is on its way.</p>",
"tags": ["order-shipped"]
}'
# MAILLAND_API_KEY=ml_pet_... (production, transactional)Field mapping for the send call
From POST /email and /email/withTemplate to POST /v1/transactional/send.
| Postmark | Mail.Land | Note |
|---|---|---|
X-Postmark-Server-Token: <token> | X-API-Key: <key> | A different header. |
From | from.email, from.name | Split the display name from the address. The address must be on a verified domain. |
To | to[].email, to[].name | An array of objects instead of a comma-separated string. Up to 5 recipients per request with a transactional key; each receives a separate copy. |
Cc, Bcc | None | Not supported. Send a separate message to each recipient. |
Subject | subject | Required unless you use a template. |
HtmlBody | htmlContent | |
TextBody | textContent | |
ReplyTo | replyTo | A single address string. If you used several comma-separated reply-to addresses, keep one. |
Tag | tags | An array, so one message can carry several tags. |
Metadata | None | There is no key and value metadata field. Use tags, or a custom header. |
Headers [{ Name, Value }] | customHeaders | An object of header names and values instead of an array. |
TrackOpens | tracking.opens | A domain's own open-tracking choice takes precedence. |
TrackLinks | tracking.clicks | A boolean instead of None, HtmlAndText, HtmlOnly or TextOnly. |
Attachments[].Name, Content, ContentType, ContentID | attachments[].fileName, content, contentType, contentId, inline | Base64 content as before. Set inline: true for an inline image. Up to 10 attachments and 10 MiB in total. |
MessageStream | None | The key decides the stream: ml_pet_ or ml_pem_. |
TemplateId, TemplateAlias | templateId | Recreate the template in Mail.Land first; it gets a new ID. |
TemplateModel | variables, to[].variables | Flat values only, because placeholders have no sections or loops. |
POST /email/batch (up to 500 messages) | POST /v1/transactional/send-batch | A different model: shared content for up to 10,000 recipients, each with their own variables, sent with a marketing key. For different messages, make one send call each. |
No equivalent | scheduledAt | Schedule a message up to 7 days ahead. |
No equivalent | X-Idempotency-Key header | Within the retention window, a retry with the same key returns the original response instead of sending again. |
Mail.Land answers 202 Accepted with a jobId, a message ID for each accepted recipient, and the lists of accepted and rejected recipients.
Add your domain and DNS records
Add the domain in Mail.Land and publish the records the dashboard shows you. SPF, DKIM and DMARC must verify before you can send. You can do this while Postmark is still sending.
| Record | Name | Value | Note |
|---|---|---|---|
| SPF (TXT) | yourdomain.com | include:spf.mlnd.net | Add the include to your existing SPF record. A domain must have only one SPF record. |
| DKIM (TXT) | mlnd._domainkey.yourdomain.com | v=DKIM1; k=rsa; p=... | The selector is mlnd, so the record sits beside Postmark's DKIM record. |
| DMARC (TXT) | _dmarc.yourdomain.com | v=DMARC1; p=... | Required. Keep a single DMARC record; an existing one with a policy is enough. |
| Return path (CNAME) | bounce.yourdomain.com | bounce.mlnd.net | Set it to DNS only, not proxied. Check that this name is not already in use. |
Keep Postmark's DNS records until the move is finished, and remove them only after its last message has gone.
Import your suppression lists first
Postmark keeps suppressions for each message stream. Export each stream's list, for example with GET /message-streams/<stream>/suppressions/dump, then import it into Mail.Land with the matching type and scope, in the dashboard or with POST /v1/transactional/suppressions/import.
| Postmark reason | Mail.Land type | Scope |
|---|---|---|
| HardBounce | HardBounce | Global |
| SpamComplaint | Complaint | Global |
| ManualSuppression on a broadcast stream (unsubscribes) | Unsubscribe | Marketing; or List, which applies only to campaigns sent to the matching Mail.Land mailing list |
| ManualSuppression on a transactional stream | Manual | Transactional |
Import before your first send through Mail.Land, so that people who opted out stay suppressed from the start. Addresses that already exist are skipped, not duplicated.
Point your webhooks at Mail.Land
Add your endpoint in Mail.Land, choose the events and verify the X-MLND-Signature header with that webhook's secret. If you protected your Postmark endpoint with basic auth or an IP allowlist, verify the signature instead.
| Postmark webhook | Mail.Land event | Note |
|---|---|---|
Delivery | message.delivered | |
Bounce | message.bounced | Covers hard and soft bounces. Temporary delays are message.deferred. |
Spam complaint | message.complained | |
Subscription change | message.unsubscribed | Sent for the recipient's own unsubscribe, not for changes you make through the API. |
Open, Click | Not available yet | Open and click events are catalogued but not sent today. |
Inbound | inbound.received | Set up an inbound route for the receiving address. |
SMTP API Error | None | The SMTP relay refuses a message during the session, with a reply code. |
Deduplicate on event_id, because a retried delivery carries the same event ID. A status can be corrected later, so keep processing events after a final one and compare status_version.
Recreate your templates
Create each template in the Mail.Land dashboard or with the templates API, then send it with templateId and variables. Every placeholder needs a value, or the send is refused with 422 and the missing names.
| Postmark | Mail.Land |
|---|---|
{{firstName}} | {{firstName}} |
{{#each items}} ... {{/each}} | None |
{{#section}}, {{^section}} | None |
Layout templates | None |
Mail.Land placeholders are plain: no sections, loops or layouts. Pass flat values, build lists in your application, and repeat shared header and footer HTML in each template.
Cut over step by step
- Add and verify your sending domain in Mail.Land, adding include:spf.mlnd.net to your existing SPF record.
- Export the suppressions of each Postmark message stream and import them.
- Create a transactional key for each application, and a marketing key if you send broadcasts.
- Recreate your templates and webhooks.
- Create a sandbox key with sandboxBehavior LogOnly, and run your integration tests against Mail.Land without delivering mail.
- Send real messages to addresses you control and check them in the email log.
- Move one application or a small share of traffic, then watch delivery, bounces and complaints.
- While both providers run, keep opt-outs in step both ways: copy new unsubscribes, complaints and bounces from Postmark to Mail.Land and from Mail.Land to Postmark, and do a final sync before you move the remaining traffic.
- Check your domain's warm-up: on Free and Pro, a new domain has a daily limit that rises in stages, and sends above it are refused. Plan the move within those limits, or ask us about a custom schedule. Max skips warm-up.
- Then move the rest, and keep Postmark active until its last messages have gone.
Sources
Postmark's public pages used for this guide.
- [1]Postmark Email APIhttps://postmarkapp.com/developer/api/email-apichecked November 4, 2026
- [2]Postmark Templates APIhttps://postmarkapp.com/developer/api/templates-apichecked November 4, 2026
- [3]Postmark: send email with SMTPhttps://postmarkapp.com/developer/user-guide/send-email-with-smtpchecked November 4, 2026
- [4]Postmark webhooks overviewhttps://postmarkapp.com/developer/webhooks/webhooks-overviewchecked November 4, 2026
- [5]Postmark Suppressions APIhttps://postmarkapp.com/developer/api/suppressions-apichecked November 4, 2026
Frequently asked questions
Can I keep sending through Postmark while I test?
Yes. Verifying a domain in Mail.Land does not stop Postmark from sending for it, so you can run both and move traffic gradually.
Does Mail.Land support cc and bcc?
Not on the send call. Each recipient receives a separate copy, so send a separate message where you used cc or bcc.
How do I replace my broadcast stream?
Use a marketing key (ml_pem_) with send-batch for API sends, or create a campaign in the dashboard on the Pro or Max plan.
Is there an official Mail.Land SDK?
Not yet. The documentation has copy-paste examples for the REST API in cURL, Node.js, Python, PHP, C#, Java and Go, and any SMTP library works with the relay.
Ready to move from Postmark?
Create a free account and send your first test today. If you want help, tell us about your setup.