Skip to main content
MailLand

Migration guide

Migrate from SendGrid to Mail.Land

Often the change is one SMTP host or one API call. This guide maps the common SendGrid fields to Mail.Land and lists the differences that need a code change.

SMTP
# Before (SendGrid)
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<your SendGrid API key>

# After (Mail.Land)
SMTP_HOST=smtp.mail.land
SMTP_PORT=587          # STARTTLS
SMTP_USER=apikey       # any value; Mail.Land ignores it
SMTP_PASS=<your Mail.Land API key with SMTP enabled>

SendGrid concepts in Mail.Land

SendGrid concepts in Mail.Land
SendGridMail.Land
API keyAPI key (credential), limited to the API or SMTP, to domains and to IP addresses
Sender authenticationDomain verification (SPF, DKIM, DMARC and return path)
Dynamic templatesTemplates with {{name}} placeholders
Suppressions and unsubscribe groupsSuppressions with a scope: all mail, marketing only, or one mailing list
Event WebhookWebhooks signed with HMAC-SHA256
Inbound ParseInbound routes

Switch SMTP: change three settings

If you send through SMTP, change the host, the port and the password. The username can stay apikey, because Mail.Land ignores it and authenticates with the key alone.

Switch SMTP: change three settings
SettingSendGridMail.Land
Hostsmtp.sendgrid.netsmtp.mail.land
Port25, 587 or 2525 (TLS), or 465 (SSL)587 with STARTTLS
UsernameapikeyAny value, for example apikey
PasswordYour SendGrid API keyA Mail.Land API key with SMTP enabled

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

SMTP relay documentation

Switch the API call

The Mail.Land send call uses a flatter JSON body and the X-API-Key header. Here is the same message in both APIs.

curl -X POST https://api.sendgrid.com/v3/mail/send \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{
      "to": [{ "email": "[email protected]", "name": "Jane" }],
      "dynamic_template_data": { "orderNumber": "12345" }
    }],
    "from": { "email": "[email protected]", "name": "Your Shop" },
    "reply_to": { "email": "[email protected]" },
    "subject": "Order 12345 has shipped",
    "content": [
      { "type": "text/plain", "value": "Your order is on its way." },
      { "type": "text/html", "value": "<p>Your order is on its way.</p>" }
    ],
    "categories": ["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 '{
    "to": [{
      "email": "[email protected]", "name": "Jane",
      "variables": { "orderNumber": "12345" }
    }],
    "from": { "email": "[email protected]", "name": "Your Shop" },
    "replyTo": "[email protected]",
    "subject": "Order 12345 has shipped",
    "textContent": "Your order is on its way.",
    "htmlContent": "<p>Your order is on its way.</p>",
    "tags": ["order-shipped"]
  }'

Field mapping for the send call

From POST /v3/mail/send to POST /v1/transactional/send.

Field mapping for the send call
SendGridMail.LandNote
Authorization: Bearer <key>X-API-Key: <key>A different header.
from.email, from.namefrom.email, from.nameThe address must be on a verified domain.
personalizations[].to[]to[]Up to 5 recipients per request with a transactional key. For bulk sending, use batch sending with a marketing key.
personalizations[].cc, bccNoneNot supported. Send a separate message to each recipient.
personalizations[].dynamic_template_datato[].variablesPer-recipient values. Use the top-level variables for values shared by every recipient.
subjectsubjectRequired unless you use a template.
content[] (text/html)htmlContent
content[] (text/plain)textContent
template_idtemplateIdRecreate the template in Mail.Land first; the ID changes.
reply_to.emailreplyToA plain address string.
categories, custom_argstagsTags appear in analytics and in webhook events.
headerscustomHeaders
attachments[].content, filename, typeattachments[].content, fileName, contentTypeBase64 content. Up to 10 attachments and 10 MiB in total.
attachments[].disposition, content_idattachments[].inline, contentIdinline: true for an inline image.
send_at (Unix time, up to 72 hours ahead)scheduledAt (UTC date and time, up to 7 days ahead)
tracking_settings (open, click)tracking.opens, tracking.clicksA domain's own tracking settings take precedence for opens.
asm.group_idNone on the send callMarketing unsubscribes are handled by Mail.Land lists and campaigns. Import existing group unsubscribes, see below.
No equivalentX-Idempotency-Key headerWithin the retention window, a retry with the same key returns the original response instead of sending again.

Both APIs answer 202 Accepted. Mail.Land's response body includes a jobId, one message ID per recipient, and the lists of accepted and rejected recipients.

REST API documentation

Import your suppression lists first

Import before your first send through Mail.Land, so that people who unsubscribed or complained stay suppressed from the start. Export each list from SendGrid as CSV, then import it with the matching type and scope, in the dashboard or with POST /v1/transactional/suppressions/import.

Import your suppression lists first
SendGrid listMail.Land typeScope
BouncesHardBounceGlobal
Spam reportsComplaintGlobal
Invalid emailsInvalidGlobal
Global unsubscribesUnsubscribeGlobal
Group unsubscribesUnsubscribeList, for the matching Mail.Land mailing list; Marketing blocks all marketing email

SendGrid blocks are usually temporary refusals. Review them before importing rather than suppressing them all.

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 your webhook's secret. Event names differ from SendGrid's, so update your handler:

Point your webhooks at Mail.Land
SendGrid eventMail.Land event
deliveredmessage.delivered
deferredmessage.deferred
bouncemessage.bounced
open, clickNot available yet
spamreportmessage.complained
unsubscribemessage.unsubscribed

Deduplicate on the event_id field: a retried delivery carries the same event ID. SendGrid's open and click events have no equivalent Mail.Land webhook today: open and click events are catalogued but not sent.

Webhook signature verification

Cut over step by step

  1. Add and verify your sending domain in Mail.Land. Keep your SendGrid records: add include:spf.mlnd.net to your existing SPF record instead of creating a second one, and keep a single DMARC record.
  2. Import your suppression lists.
  3. Create a sandbox key set to log only, and run your integration tests against Mail.Land without delivering mail.
  4. Send real messages to addresses you control and check them in the email log.
  5. Move one application or a small share of traffic, then watch delivery, bounces and complaints.
  6. While both providers run, copy new unsubscribes, complaints and bounces from SendGrid to Mail.Land, and do a final sync before you switch the last traffic.
  7. Move the rest, and keep SendGrid active until its last scheduled and retried messages have gone.

Frequently asked questions

Can I keep sending through SendGrid while I test?

Yes. Verifying a domain in Mail.Land does not stop SendGrid 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. Send a separate message to each recipient instead.

Is there an official Mail.Land SDK?

Not yet. The documentation has copy-paste examples for the REST API in eight languages, and any SMTP library works with the relay.

What happens to my unsubscribe groups?

Import each group's unsubscribes with the List scope for the matching Mail.Land mailing list, and send to that list so the preference applies. The Marketing scope is the broader fallback: it blocks all marketing email to that address.

Ready to move from SendGrid?

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