Skip to main content
MailLand

Migration guide

Migrate from Amazon SES to Mail.Land

Moving from SES replaces AWS credentials, configuration sets and event destinations with one API key and signed webhooks. This guide maps each common SendEmail field and lists what needs a code change.

SMTP
# Before (Amazon SES)
SMTP_HOST=email-smtp.eu-central-1.amazonaws.com   # your Region's endpoint
SMTP_PORT=587          # STARTTLS (465 for TLS wrapper)
SMTP_USER=<SES SMTP user name for that Region>
SMTP_PASS=<SES SMTP password for that Region>

# After (Mail.Land)
SMTP_HOST=smtp.mail.land
SMTP_PORT=587          # STARTTLS; 465 is not offered
SMTP_USER=apikey       # any value; Mail.Land ignores it
SMTP_PASS=ml_pet_...   # a Mail.Land key with SMTP enabled

Amazon SES concepts in Mail.Land

Amazon SES concepts in Mail.Land
Amazon SESMail.Land
IAM user and SigV4 request signingAPI key (credential) in the X-API-Key header
SMTP credentials per AWS RegionThe same API key, used as the SMTP password
Verified identity (domain) and custom MAIL FROMVerified domain (SPF, DKIM, DMARC and return path)
Configuration sets and event destinations (SNS, EventBridge, Firehose)Webhooks signed with HMAC-SHA256, sent straight to your endpoint
Account-level suppression listSuppressions with a type and a scope
Email templates (Handlebars)Templates with {{name}} placeholders
Contact lists and topicsMailing lists, campaigns and unsubscribe handling (Pro and Max)
SandboxSandbox keys (ml_set_, ml_sem_): with sandboxBehavior LogOnly, requests are accepted and logged but never delivered

Create the right API keys

There are no IAM policies to write. A Mail.Land key's prefix tells you its mode and its stream; create one key per application and stream, and limit it to the domains and IP addresses that use it.

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

SES SMTP credentials are created through IAM and are different in each AWS Region. Mail.Land uses your API key as the password and ignores the username.

Switch SMTP: change the host and the credentials
SettingAmazon SESMail.Land
Hostemail-smtp.<region>.amazonaws.comsmtp.mail.land
Port25, 587 or 2587 with STARTTLS, or 465 and 2465 with TLS wrapper587 with STARTTLS
UsernameSMTP user name for that RegionAny value, for example apikey
PasswordSMTP password for that RegionA Mail.Land API key with SMTP enabled (ml_pet_ for transactional mail)

Mail.Land accepts sign-in only after STARTTLS, and ports 465 and 2465 are not offered. If your application uses TLS wrapper, switch it to 587 with STARTTLS. Mail.Land does not act on X-SES-* headers such as X-SES-CONFIGURATION-SET, so you can remove them. Messages can be up to 15 MiB.

SMTP relay documentation

Switch the API call

SES requests are signed with AWS Signature Version 4, usually through an AWS SDK. Mail.Land needs only the X-API-Key header and a flatter JSON body. Here is the same message in both, and a template send.

aws sesv2 send-email --region eu-central-1 \
  --from-email-address "[email protected]" \
  --destination '{"ToAddresses":["[email protected]"]}' \
  --reply-to-addresses "[email protected]" \
  --email-tags Name=type,Value=order-shipped \
  --content '{
    "Simple": {
      "Subject": { "Data": "Order 12345 has shipped" },
      "Body": {
        "Text": { "Data": "Your order is on its way." },
        "Html": { "Data": "<p>Your order is on its way.</p>" }
      }
    }
  }'
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 SES API v2 SendEmail (POST /v2/email/outbound-emails) to POST /v1/transactional/send.

Field mapping for the send call
Amazon SESMail.LandNote
AWS Signature Version 4X-API-Key: <key>No request signing; send the key over HTTPS.
FromEmailAddressfrom.email, from.nameThe address must be on a verified domain.
Destination.ToAddressesto[].email, to[].nameUp to 5 recipients per request with a transactional key; each receives a separate copy.
Destination.CcAddresses, BccAddressesNoneNot supported. Send a separate message to each recipient.
ReplyToAddressesreplyToA single address string instead of a list. If you used several, keep one.
Content.Simple.Subject.DatasubjectRequired unless you use a template.
Content.Simple.Body.Html.DatahtmlContent
Content.Simple.Body.Text.DatatextContent
Content.Simple.Headers [{ Name, Value }]customHeadersAn object of header names and values instead of an array.
Content.Simple.Attachments[].FileName, ContentType, RawContent, ContentId, ContentDispositionattachments[].fileName, contentType, content, contentId, inlineBase64 content. Set inline: true for an inline image. Up to 10 attachments and 10 MiB in total.
Content.Raw.Data (MIME)None on the APISend ready-made MIME messages through the SMTP relay instead.
Content.Template.TemplateName, TemplateArntemplateIdRecreate the template in Mail.Land first.
Content.Template.TemplateDatavariables, to[].variablesA JSON object instead of a JSON string.
EmailTags [{ Name, Value }]tagsAn array of strings instead of name and value pairs.
ConfigurationSetNameNoneWebhooks and tracking are set in your Mail.Land account and on each domain.
ConfigurationOverrides.Trackingtracking.opens, tracking.clicksA domain's own open-tracking choice takes precedence.
ListManagementOptionsNone on the send callMarketing unsubscribes are handled by Mail.Land lists and campaigns.
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.

SES answers 200 with a MessageId. 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 SES 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 the DKIM records SES uses.
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. If your SES custom MAIL FROM domain already uses this name, keep it until SES stops sending.

Keep the SES DNS records until the move is finished, and remove them only after its last message has gone.

DNS setup documentation

Import your suppression list first

The SES account-level suppression list applies per AWS Region. Export it from each Region you send from with ListSuppressedDestinations (aws sesv2 list-suppressed-destinations), 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 list first
SES reason or sourceMail.Land typeScope
BOUNCEHardBounceGlobal
COMPLAINTComplaintGlobal
Contact list topic unsubscribesUnsubscribeMarketing; or List, which applies only to campaigns sent to the matching Mail.Land mailing list

SES adds only hard bounces to the list, and Gmail spam reports do not reach it, so also import any suppressions your own systems keep. Import before your first send through Mail.Land, so that people who opted out stay suppressed from the start.

Suppressions documentation

Point your webhooks at Mail.Land

Replace the configuration set and its event destination with a Mail.Land webhook: add your HTTPS endpoint, choose the events and verify the X-MLND-Signature header with that webhook's secret. There is no SNS subscription to confirm.

Point your webhooks at Mail.Land
SES eventMail.Land eventNote
SendNone todaymessage.sent is not available yet; the 202 response confirms acceptance.
Deliverymessage.delivered
DeliveryDelaymessage.deferred
Bouncemessage.bounced
Complaintmessage.complained
Subscriptionmessage.unsubscribedSent for the recipient's own unsubscribe, not for changes you make through the API.
RejectNone
RenderingFailureNoneA missing template value is refused when you call the API, with 422 TEMPLATE_VARIABLE_MISSING.
Open, ClickNot 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
Amazon SESMail.Land
{{firstName}}{{firstName}}
{{contact.firstName}} (nested){{firstName}}
{{#each}}, {{#if}} ... {{else}}None
Inline partials ({{> name}})None

Mail.Land placeholders are plain: no nested values, conditions, loops or partials. Pass flat values and move the 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 list of each AWS Region you use and import it.
  3. Create Mail.Land keys for each application and stream, limited to your domains and IP addresses.
  4. Recreate your templates, and replace each event destination with a webhook.
  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 SES to Mail.Land and from Mail.Land to SES, 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 SES active until its last messages have gone.

Mail.Land vs Amazon SES: the sourced comparison

Sources

AWS's public pages used for this guide.

  1. [1]Amazon SES API v2: SendEmailhttps://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.htmlchecked November 4, 2026
  2. [2]Amazon SES: connecting to an SMTP endpointhttps://docs.aws.amazon.com/ses/latest/dg/smtp-connect.htmlchecked November 4, 2026
  3. [3]Amazon SES: obtaining SMTP credentialshttps://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.htmlchecked November 4, 2026
  4. [4]Amazon SES: advanced email personalizationhttps://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-advanced.htmlchecked November 4, 2026
  5. [5]Amazon SES: event publishinghttps://docs.aws.amazon.com/ses/latest/dg/monitor-using-event-publishing.htmlchecked November 4, 2026
  6. [6]Amazon SES: account-level suppression listhttps://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list.htmlchecked November 4, 2026

Frequently asked questions

Can I keep sending through SES while I test?

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

Do I still need an AWS SDK?

No. Any HTTP client can call the Mail.Land API with the X-API-Key header, and any SMTP library works with the relay.

How do I send bulk mail that I sent with SendBulkEmail?

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

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.

Ready to move from Amazon SES?

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