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.
# 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 enabledAmazon SES concepts in Mail.Land
| Amazon SES | Mail.Land |
|---|---|
| IAM user and SigV4 request signing | API key (credential) in the X-API-Key header |
| SMTP credentials per AWS Region | The same API key, used as the SMTP password |
| Verified identity (domain) and custom MAIL FROM | Verified 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 list | Suppressions with a type and a scope |
| Email templates (Handlebars) | Templates with {{name}} placeholders |
| Contact lists and topics | Mailing lists, campaigns and unsubscribe handling (Pro and Max) |
| Sandbox | Sandbox 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.
| 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
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.
| Setting | Amazon SES | Mail.Land |
|---|---|---|
| Host | email-smtp.<region>.amazonaws.com | smtp.mail.land |
| Port | 25, 587 or 2587 with STARTTLS, or 465 and 2465 with TLS wrapper | 587 with STARTTLS |
| Username | SMTP user name for that Region | Any value, for example apikey |
| Password | SMTP password for that Region | A 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.
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.
| Amazon SES | Mail.Land | Note |
|---|---|---|
AWS Signature Version 4 | X-API-Key: <key> | No request signing; send the key over HTTPS. |
FromEmailAddress | from.email, from.name | The address must be on a verified domain. |
Destination.ToAddresses | to[].email, to[].name | Up to 5 recipients per request with a transactional key; each receives a separate copy. |
Destination.CcAddresses, BccAddresses | None | Not supported. Send a separate message to each recipient. |
ReplyToAddresses | replyTo | A single address string instead of a list. If you used several, keep one. |
Content.Simple.Subject.Data | subject | Required unless you use a template. |
Content.Simple.Body.Html.Data | htmlContent | |
Content.Simple.Body.Text.Data | textContent | |
Content.Simple.Headers [{ Name, Value }] | customHeaders | An object of header names and values instead of an array. |
Content.Simple.Attachments[].FileName, ContentType, RawContent, ContentId, ContentDisposition | attachments[].fileName, contentType, content, contentId, inline | Base64 content. Set inline: true for an inline image. Up to 10 attachments and 10 MiB in total. |
Content.Raw.Data (MIME) | None on the API | Send ready-made MIME messages through the SMTP relay instead. |
Content.Template.TemplateName, TemplateArn | templateId | Recreate the template in Mail.Land first. |
Content.Template.TemplateData | variables, to[].variables | A JSON object instead of a JSON string. |
EmailTags [{ Name, Value }] | tags | An array of strings instead of name and value pairs. |
ConfigurationSetName | None | Webhooks and tracking are set in your Mail.Land account and on each domain. |
ConfigurationOverrides.Tracking | tracking.opens, tracking.clicks | A domain's own open-tracking choice takes precedence. |
ListManagementOptions | None on the send call | Marketing unsubscribes are handled by Mail.Land lists and campaigns. |
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. |
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.
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.
| 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 the DKIM records SES uses. |
| 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. 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.
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.
| SES reason or source | Mail.Land type | Scope |
|---|---|---|
| BOUNCE | HardBounce | Global |
| COMPLAINT | Complaint | Global |
| Contact list topic unsubscribes | Unsubscribe | Marketing; 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.
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.
| SES event | Mail.Land event | Note |
|---|---|---|
Send | None today | message.sent is not available yet; the 202 response confirms acceptance. |
Delivery | message.delivered | |
DeliveryDelay | message.deferred | |
Bounce | message.bounced | |
Complaint | message.complained | |
Subscription | message.unsubscribed | Sent for the recipient's own unsubscribe, not for changes you make through the API. |
Reject | None | |
RenderingFailure | None | A missing template value is refused when you call the API, with 422 TEMPLATE_VARIABLE_MISSING. |
Open, Click | Not available yet | Open 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.
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.
| Amazon SES | Mail.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.
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 suppression list of each AWS Region you use and import it.
- Create Mail.Land keys for each application and stream, limited to your domains and IP addresses.
- Recreate your templates, and replace each event destination with a webhook.
- 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 SES to Mail.Land and from Mail.Land to SES, 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 SES active until its last messages have gone.
Sources
AWS's public pages used for this guide.
- [1]Amazon SES API v2: SendEmailhttps://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.htmlchecked November 4, 2026
- [2]Amazon SES: connecting to an SMTP endpointhttps://docs.aws.amazon.com/ses/latest/dg/smtp-connect.htmlchecked November 4, 2026
- [3]Amazon SES: obtaining SMTP credentialshttps://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.htmlchecked November 4, 2026
- [4]Amazon SES: advanced email personalizationhttps://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-advanced.htmlchecked November 4, 2026
- [5]Amazon SES: event publishinghttps://docs.aws.amazon.com/ses/latest/dg/monitor-using-event-publishing.htmlchecked November 4, 2026
- [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.