Skip to main content
MailLand

Migration guide

Migrate from Mailgun to Mail.Land

If you send through SMTP, the change is a host and a password. If you use the messages API, this guide maps each common Mailgun field to Mail.Land and lists what needs a code change.

SMTP
# Before (Mailgun)
SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587          # STARTTLS
SMTP_USER=<SMTP login of your sending domain>
SMTP_PASS=<SMTP password of that login>

# 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_...   # a Mail.Land key with SMTP enabled

Mailgun concepts in Mail.Land

Mailgun concepts in Mail.Land
MailgunMail.Land
Sending domainVerified domain (SPF, DKIM, DMARC and return path)
API key and domain sending keysAPI keys (credentials), each limited to the API or SMTP, to domains and to IP addresses
SMTP credentials per domainThe same API key, used as the SMTP password
Templates (Handlebars)Templates with {{name}} placeholders
Bounces, complaints and unsubscribes listsSuppressions with a type and a scope
WebhooksWebhooks signed with HMAC-SHA256, a secret per webhook
RoutesInbound routes

Create the right API keys

A Mail.Land key's prefix tells you its mode and its stream. Create one key per application and stream, and keep the production keys out of your test systems.

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 password

Mailgun uses an SMTP login per sending domain. Mail.Land uses your API key as the password and ignores the username.

Switch SMTP: change the host and the password
SettingMailgunMail.Land
Hostsmtp.mailgun.orgsmtp.mail.land
Port25, 587 or 2525 with STARTTLS, or 465 with TLS587 with STARTTLS
UsernameThe SMTP login of your sending domainAny value, for example apikey
PasswordThe password of that SMTP loginA Mail.Land API key with SMTP enabled (ml_pet_ for transactional mail)

Mail.Land accepts sign-in only after STARTTLS, and port 465 is not offered. If your application uses 465, switch it to 587 with STARTTLS. Messages can be up to 15 MiB.

SMTP relay documentation

Switch the API call

Mailgun takes form fields with HTTP Basic auth. Mail.Land takes a JSON body with the X-API-Key header. Here is the same message in both APIs, and a batch send.

curl -s --user "api:$MAILGUN_API_KEY" \
  https://api.mailgun.net/v3/yourdomain.com/messages \
  -F from="Your Shop <[email protected]>" \
  -F to="Jane <[email protected]>" \
  -F subject="Order 12345 has shipped" \
  -F text="Your order is on its way." \
  -F html="<p>Your order is on its way.</p>" \
  -F h:Reply-To="[email protected]" \
  -F o:tag="order-shipped"
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 /v3/<domain>/messages to POST /v1/transactional/send.

Field mapping for the send call
MailgunMail.LandNote
--user api:<key> (Basic auth)X-API-Key: <key>A header instead of Basic auth, and a JSON body instead of form fields.
fromfrom.email, from.nameSplit the display name from the address. The address must be on a verified domain.
toto[].email, to[].nameUp 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.
texttextContent
htmlhtmlContent
amp-htmlNoneThere is no AMP part.
template, t:versiontemplateIdRecreate the template in Mail.Land first. Templates have no versions.
t:variablesvariablesValues shared by every recipient, as a JSON object instead of a JSON string.
v:<name> (metadata)tagsMail.Land has no key and value metadata on events. Carry an identifier in tags or in a custom header.
recipient-variablesto[].variablesPer-recipient values. For more than 5 recipients, use send-batch with a marketing key and recipients[].variables.
o:tagtagsAn array of strings.
h:<Header-Name>customHeadersAn object of header names and values.
h:Reply-ToreplyToA plain address string.
attachment, inlineattachments[].fileName, contentType, content, contentId, inlineBase64 content in JSON instead of multipart files. Up to 10 attachments and 10 MiB in total.
o:deliverytimescheduledAtA UTC date and time in ISO 8601, up to 7 days ahead.
o:tracking-opens, o:tracking-clickstracking.opens, tracking.clicksBooleans. A domain's own open-tracking choice takes precedence.
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 Mailgun 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 Mailgun'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 Mailgun'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

Mailgun keeps bounces, complaints and unsubscribes for each sending domain, so export the lists of every domain you send from, for example with GET /v3/<domain>/bounces. Then import each list into Mail.Land with the matching type and scope, in the dashboard or with POST /v1/transactional/suppressions/import.

Import your suppression lists first
Mailgun listMail.Land typeScope
BouncesHardBounceGlobal
ComplaintsComplaintGlobal
Unsubscribes (all mail)UnsubscribeGlobal
Unsubscribes for one tag or mailing listUnsubscribeMarketing; or List, which applies only to campaigns sent to the matching Mail.Land mailing list

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. Mailgun signs a timestamp and a token with one account-wide key; Mail.Land signs the timestamp and the raw body with a secret per webhook, so update your verification code too.

Point your webhooks at Mail.Land
Mailgun eventMail.Land eventNote
acceptedNone todaymessage.sent is not available yet; the 202 response confirms acceptance.
deliveredmessage.delivered
temporary_failmessage.deferred
permanent_failmessage.bouncedA refusal by the receiving server. message.failed reports a stop on Mail.Land's side.
complainedmessage.complained
unsubscribedmessage.unsubscribedSent for the recipient's own unsubscribe, not for changes you make through the API.
opened, clickedNot available yetOpen and click events are catalogued but not sent today.

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
MailgunMail.Land
{{firstName}}{{firstName}}
%recipient.firstName%{{firstName}}
{{#if}}, {{#unless}}, {{#each}}, {{#with}}, {{#equal}}None
Template versions (t:version)None

Mail.Land placeholders are plain: no conditions, loops, filters or partials. Move that logic into your application, or keep one template per variant.

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 suppression lists of each Mailgun domain and import them.
  3. Recreate your templates and webhooks.
  4. Create a sandbox key with sandboxBehavior LogOnly, and run your integration tests against Mail.Land without delivering mail.
  5. Send real messages to addresses you control and check them in the email log.
  6. Move one application or a small share of traffic, then watch delivery, bounces and complaints.
  7. While both providers run, keep opt-outs in step both ways: copy new unsubscribes, complaints and bounces from Mailgun to Mail.Land and from Mail.Land to Mailgun, and do a final sync before you move the remaining traffic.
  8. 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.
  9. Then move the rest, and keep Mailgun active until its last scheduled and retried messages have gone.

Mail.Land vs Mailgun: the sourced comparison

Sources

Mailgun's public pages used for this guide.

  1. [1]Mailgun API: send a messagehttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messageschecked November 4, 2026
  2. [2]Mailgun API: authenticationhttps://documentation.mailgun.com/docs/mailgun/api-reference/authenticationchecked November 4, 2026
  3. [3]Mailgun: send with SMTPhttps://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/send-smtpchecked November 4, 2026
  4. [4]Mailgun: send with templateshttps://documentation.mailgun.com/docs/mailgun/user-manual/sending-messages/send-templateschecked November 4, 2026
  5. [5]Mailgun: webhookshttps://documentation.mailgun.com/docs/mailgun/user-manual/webhooks/webhookschecked November 4, 2026
  6. [6]Mailgun: securing webhookshttps://documentation.mailgun.com/docs/mailgun/user-manual/webhooks/securing-webhookschecked November 4, 2026
  7. [7]Mailgun API: bounceshttps://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/bounces/get-v3--domainid--bounceschecked November 4, 2026

Frequently asked questions

Can I keep sending through Mailgun while I test?

Yes. Verifying a domain in Mail.Land does not stop Mailgun from sending for it, so you can run both and move traffic gradually.

Mailgun lets me choose a US or EU region. Where does Mail.Land run?

Mail.Land has no regions to choose. The primary copy of your data is in Germany; the providers outside the EU are listed on our GDPR page.

How do I send to many recipients at once?

Use POST /v1/transactional/send-batch with a marketing key (ml_pem_). It takes up to 10,000 recipients, each with their own variables. A transactional key takes up to 5 recipients per request.

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 Mailgun?

Create a free account and send your first test today. If you want help, tell us about your setup.