Skip to main content
MailLand

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.

SMTP
# 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 broadcasts

Postmark concepts in Mail.Land

Postmark concepts in Mail.Land
PostmarkMail.Land
Server and Server API tokenWorkspace 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 domainsVerified domain (SPF, DKIM, DMARC and return path)
Templates and layouts (Mustachio)Templates with {{name}} placeholders
Suppressions per message streamSuppressions with a type and a scope
WebhooksWebhooks signed with HMAC-SHA256, a secret per webhook
Inbound streamInbound 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.

Create the right API keys
PrefixWhat 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.

API keys documentation

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.

Switch SMTP: change the host and the credentials
SettingPostmarkMail.Land
Hostsmtp.postmarkapp.com, or smtp-broadcasts.postmarkapp.com for broadcastssmtp.mail.land
Port25, 587 or 2525, with STARTTLS587 with STARTTLS
UsernameServer API token, or SMTP token access keyAny value, for example apikey
PasswordServer API token, or SMTP token secret keyA Mail.Land API key with SMTP enabled
StreamX-PM-Message-Stream headerThe 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.

SMTP relay documentation

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.

Field mapping for the send call
PostmarkMail.LandNote
X-Postmark-Server-Token: <token>X-API-Key: <key>A different header.
Fromfrom.email, from.nameSplit the display name from the address. The address must be on a verified domain.
Toto[].email, to[].nameAn 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, BccNoneNot supported. Send a separate message to each recipient.
SubjectsubjectRequired unless you use a template.
HtmlBodyhtmlContent
TextBodytextContent
ReplyToreplyToA single address string. If you used several comma-separated reply-to addresses, keep one.
TagtagsAn array, so one message can carry several tags.
MetadataNoneThere is no key and value metadata field. Use tags, or a custom header.
Headers [{ Name, Value }]customHeadersAn object of header names and values instead of an array.
TrackOpenstracking.opensA domain's own open-tracking choice takes precedence.
TrackLinkstracking.clicksA boolean instead of None, HtmlAndText, HtmlOnly or TextOnly.
Attachments[].Name, Content, ContentType, ContentIDattachments[].fileName, content, contentType, contentId, inlineBase64 content as before. Set inline: true for an inline image. Up to 10 attachments and 10 MiB in total.
MessageStreamNoneThe key decides the stream: ml_pet_ or ml_pem_.
TemplateId, TemplateAliastemplateIdRecreate the template in Mail.Land first; it gets a new ID.
TemplateModelvariables, to[].variablesFlat values only, because placeholders have no sections or loops.
POST /email/batch (up to 500 messages)POST /v1/transactional/send-batchA 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 equivalentscheduledAtSchedule a message up to 7 days ahead.
No equivalentX-Idempotency-Key headerWithin 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.

REST API documentation

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.

Add your domain and DNS records
RecordNameValueNote
SPF (TXT)yourdomain.cominclude:spf.mlnd.netAdd the include to your existing SPF record. A domain must have only one SPF record.
DKIM (TXT)mlnd._domainkey.yourdomain.comv=DKIM1; k=rsa; p=...The selector is mlnd, so the record sits beside Postmark's DKIM record.
DMARC (TXT)_dmarc.yourdomain.comv=DMARC1; p=...Required. Keep a single DMARC record; an existing one with a policy is enough.
Return path (CNAME)bounce.yourdomain.combounce.mlnd.netSet 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.

DNS setup documentation

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.

Import your suppression lists first
Postmark reasonMail.Land typeScope
HardBounceHardBounceGlobal
SpamComplaintComplaintGlobal
ManualSuppression on a broadcast stream (unsubscribes)UnsubscribeMarketing; or List, which applies only to campaigns sent to the matching Mail.Land mailing list
ManualSuppression on a transactional streamManualTransactional

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.

Suppressions documentation

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.

Point your webhooks at Mail.Land
Postmark webhookMail.Land eventNote
Deliverymessage.delivered
Bouncemessage.bouncedCovers hard and soft bounces. Temporary delays are message.deferred.
Spam complaintmessage.complained
Subscription changemessage.unsubscribedSent for the recipient's own unsubscribe, not for changes you make through the API.
Open, ClickNot available yetOpen and click events are catalogued but not sent today.
Inboundinbound.receivedSet up an inbound route for the receiving address.
SMTP API ErrorNoneThe 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.

Webhook events

Webhook signature verification

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.

Recreate your templates
PostmarkMail.Land
{{firstName}}{{firstName}}
{{#each items}} ... {{/each}}None
{{#section}}, {{^section}}None
Layout templatesNone

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.

Templates documentation

Cut over step by step

  1. Add and verify your sending domain in Mail.Land, adding include:spf.mlnd.net to your existing SPF record.
  2. Export the suppressions of each Postmark message stream and import them.
  3. Create a transactional key for each application, and a marketing key if you send broadcasts.
  4. Recreate your templates and webhooks.
  5. Create a sandbox key with sandboxBehavior LogOnly, and run your integration tests against Mail.Land without delivering mail.
  6. Send real messages to addresses you control and check them in the email log.
  7. Move one application or a small share of traffic, then watch delivery, bounces and complaints.
  8. 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.
  9. 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.
  10. Then move the rest, and keep Postmark active until its last messages have gone.

Mail.Land vs Postmark: the sourced comparison

Sources

Postmark's public pages used for this guide.

  1. [1]Postmark Email APIhttps://postmarkapp.com/developer/api/email-apichecked November 4, 2026
  2. [2]Postmark Templates APIhttps://postmarkapp.com/developer/api/templates-apichecked November 4, 2026
  3. [3]Postmark: send email with SMTPhttps://postmarkapp.com/developer/user-guide/send-email-with-smtpchecked November 4, 2026
  4. [4]Postmark webhooks overviewhttps://postmarkapp.com/developer/webhooks/webhooks-overviewchecked November 4, 2026
  5. [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.