Migration guide
Migrate from SendGrid
SendGrid's v3 Mail Send is built around personalizations: one request, many recipients, each with their own template data. YourMail has neither personalizations nor stored templates, so those two parts change shape — each personalization becomes one message, sent alone or in a batch, and each template becomes code you render yourself. The rest is a rename. If you are still deciding, read YourMail vs SendGrid first, including the part about what you lose.
Before you change any code
- Verify the domain with the three DKIM CNAME records in Domains. Leave SendGrid's records alone until you are done with it.
- Import the suppression list. See below — this is the step people skip.
- Make a test send following the quickstart.
Then plan the timing. A new account's daily cap ramps up over its first seven days of live sending, and it is counted in recipients, not messages — the stages are on Limits. A SendGrid integration that fans one request out to thousands of personalizations is exactly the shape that meets it, so cut over ahead of a launch, not on the morning of one.
The send call
await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [
{ to: [{ email: "ana@example.com" }], dynamic_template_data: { name: "Ana" } },
{ to: [{ email: "ben@example.com" }], dynamic_template_data: { name: "Ben" } },
],
from: { email: "hello@acme.com", name: "Acme" },
template_id: "d-xxxxxxxx",
categories: ["welcome"],
custom_args: { campaign: "onboarding" },
}),
});import { YourMail } from "yourmail";
const yourmail = new YourMail(process.env.YOURMAIL_API_KEY);
// One personalization = one message. Render each body yourself.
const { data, summary } = await yourmail.batch(
users.map((user) => ({
from: "Acme <hello@acme.com>",
to: user.email,
subject: `Welcome, ${user.name}`,
html: renderWelcome({ name: user.name }),
tags: [
{ name: "category", value: "welcome" },
{ name: "campaign", value: "onboarding" },
],
})),
);
if (summary.failed > 0) console.error(data.filter((r) => "error" in r));// A single message, without the SDK.
const res = await fetch("https://api.yourmail.dev/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.YOURMAIL_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `welcome-${user.id}`,
},
body: JSON.stringify({
from: "Acme <hello@acme.com>",
to: user.email,
subject: `Welcome, ${user.name}`,
html: renderWelcome({ name: user.name }),
text: renderWelcomeText({ name: user.name }),
tags: [{ name: "category", value: "welcome" }],
}),
});
const { id } = await res.json(); // 200 { id }, not 202 with an empty bodyPOST https://api.sendgrid.com/v3/mail/send answers 202 Accepted. POST https://api.yourmail.dev/v1/emails answers with the message's id, which you can look up later; a batch answers 200 with an id or an error per item, so read summary.failed. Both authenticate with a Bearer key. The SDK is npm i yourmail; references are Send email and Batch send.
Field by field
SendGrid's field names are as of October 2026, from their v3 Mail Send reference (opens in a new tab).
personalizations→ one send each- There is no personalizations array. Each personalization becomes its own message — a
send, or an item in a batch of up to 100. One message carries at most 50 recipients across to, cc and bcc, and every one of them sees the same body. personalizations[].to, cc, bcc→ to, cc, bcc- Arrays of
{ email, name }become strings:"Ana <ana@example.com>", or an array of them. from→ from{ email, name }becomes"Acme <hello@acme.com>". Must be on a domain you have verified with YourMail.reply_to→ replyTo- One address, as a string.
reply_to_listhas no equivalent. subject→ subject- Top level only. A per-personalization subject means a per-message send.
content→ html, text- The
text/plainentry's value becomestext, thetext/htmlonehtml. 1 MB combined. template_id, dynamic_template_datanone- No stored templates. Render the body in your own code and send the result — see below.
categories→ tags- Each category becomes a
{ name, value }entry intags. Up to 10 tags per message. custom_args→ tags- Key/value pairs fold into the same tags array, sharing the 10-tag cap. Tags come back on the message and in webhook payloads.
headers→ headers- An object of string values, up to 10; headers the pipeline owns are refused with a 400.
attachments→ attachmentscontent(base64),filenameandtypebecomecontent,filenameandcontentType.dispositionandcontent_idhave no equivalent. Up to 20 files, 10 MB each, 25 MB total.send_at→ scheduledAt- SendGrid takes a Unix timestamp in seconds.
scheduledAttakes ISO 8601 or Unix milliseconds, up to 30 days ahead — Unix seconds are a 400, so multiply by 1000.batch_idhas no equivalent; cancel one message withyourmail.cancel(id). asm→ bulk- No unsubscribe groups. Non-transactional mail sets
bulk: true, which adds one-clickList-Unsubscribeheaders and honours unsubscribes for bulk mail only. tracking_settings, mail_settingsnone- No per-message settings. Open and click tracking is a per-domain setting, toggled in the dashboard.
Dynamic templates become your own rendering
A template_id with dynamic_template_data has no counterpart, because YourMail stores no templates. Move each template into your codebase and render it per recipient. If you write them as react-email components, the SDK renders them for you:
// Was: template_id "d-…" + dynamic_template_data { name }
// Now: a react-email component in your codebase.
import { Welcome } from "./emails/welcome";
await yourmail.send({
from: "Acme <hello@acme.com>",
to: user.email,
subject: `Welcome, ${user.name}`,
react: <Welcome name={user.name} />, // needs @react-email/render
});Any other renderer works the same way — produce html (and ideally text) and send it. The React Email guide has the setup.
Bring the suppression list
SendGrid keeps separate suppression lists. As of October 2026 its API returns bounces from GET /v3/suppression/bounces, spam reports from GET /v3/suppression/spam_reports and global unsubscribes from GET /v3/suppression/unsubscribes, each entry carrying an email — see their bounces reference (opens in a new tab). Import the addresses with POST /v1/suppressions before your first live send:
import { YourMail, MAX_SUPPRESSION_IMPORT } from "yourmail";
const yourmail = new YourMail(process.env.YOURMAIL_API_KEY); // full-scope key
// From SendGrid (paginate with limit/offset):
// GET https://api.sendgrid.com/v3/suppression/bounces → [{ email, … }]
// GET https://api.sendgrid.com/v3/suppression/spam_reports → [{ email, … }]
// collect the email fields into `exported`, then:
for (let i = 0; i < exported.length; i += MAX_SUPPRESSION_IMPORT) {
await yourmail.createSuppressions(exported.slice(i, i + MAX_SUPPRESSION_IMPORT));
}Bounces and spam reports are the ones that must come across. Global unsubscribes need a decision: every import is recorded as manual, and manual blocks all mail, so an imported unsubscribe would also stop that person's receipts and password resets. Why the list matters at all is in Bounces and suppression lists.
Webhooks
Point your event handling at a YourMail endpoint (dashboard, or POST /v1/webhooks). Each delivery is one event, signed in a yourmail-signature header that the SDK's constructWebhookEvent verifies against the raw body. Events are named email.delivered, email.bounced, email.complained, email.opened, email.clicked and so on, and carry the tags you set — so a handler keyed on a category reads it from the tags instead. See Webhooks.
What has no equivalent here
Marketing Campaigns, contacts and lists. YourMail is transactional sending only. bulk: true makes an individual non-transactional send compliant; it does not manage a list.
Stored templates, unsubscribe groups and per-message settings. Covered above: render in code, use bulk, and set tracking per domain.
Dedicated IPs and a deliverability team. Mail goes out through AWS SES, signed for your own domain. If those matter to you, the comparison page is candid about what you give up by leaving.
Common questions
Do I need to re-verify my sending domain?
Yes. Your domain authentication with SendGrid proves the domain to SendGrid only. YourMail asks for three DKIM CNAME records, shown when you add the domain in the dashboard or with POST /v1/domains. Nothing in that setup asks you to remove SendGrid's records, so both can be verified while you cut over.
Can I keep my SendGrid suppression list?
Yes, and it is the step that matters most. Export the bounces and spam reports through SendGrid's suppression API and import the addresses with POST /v1/suppressions — up to 100 per request, or createSuppressions() in the SDK — before the first live send. Imports are recorded with the reason manual, which blocks every send to that address.
How do I replace dynamic templates?
Render the body in your own code. YourMail stores no templates and has no template_id; you send finished html and text, or pass a React element through the SDK's react option, which renders a react-email component with the optional @react-email/render peer dependency.
Can I run SendGrid and YourMail side by side during the cutover?
Yes — move one touchpoint at a time and leave the rest on SendGrid until each is seen working. Plan the timing around the new-account ramp: a YourMail account's daily limit rises over its first week of live sending, counted in recipients, so cut over ahead of a launch rather than on the morning of it.
Switching from another provider?
- AWS SES: YourMail vs AWS SES
- Resend: migration guide · YourMail vs Resend
- Postmark: migration guide · YourMail vs Postmark
- SendGrid: migration guide · YourMail vs SendGrid
Last updated