Migrate from SendGrid to emailr_
Move from SendGrid to emailr_: map dynamic templates, categories, suppressions and Event Webhook events, swap the SDK and cut over with zero downtime.
This guide moves a SendGrid integration to emailr_ without a sending gap. You'll swap @sendgrid/mail for the emailr SDK, translate dynamic templates and categories, carry your suppressions over and rebuild the Event Webhook.
Most teams can run both providers side by side: verify your domain on emailr_, move one message type at a time, then switch the rest once the numbers look right.
The code change
A typical SendGrid send call next to its emailr_ equivalent.
import sgMail from '@sendgrid/mail'
sgMail.setApiKey(process.env.SENDGRID_API_KEY!)
await sgMail.send({
to: 'user@example.com',
from: 'hello@mail.acme.com',
subject: 'Your receipt',
html: '<p>Thanks for your order.</p>',
categories: ['receipts'],
})import { Emailr } from 'emailr'
const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! })
await emailr.emails.send({
to: 'user@example.com',
from: 'hello@mail.acme.com',
subject: 'Your receipt',
html: '<p>Thanks for your order.</p>',
tags: { category: 'receipts' },
})SendGrid concepts in emailr_
Use this table to find where each feature you rely on lives in emailr_.
| SendGrid | emailr_ | Notes |
|---|---|---|
API key (SG. prefix) | API key (et_live_ prefix) | Keys are scoped to a domain workspace. |
| Domain authentication (CNAMEs) | Domain with DKIM, SPF and send. MAIL FROM records | |
Dynamic templates (d-… ID, Handlebars) | Templates (template_id + template_data) | Flat {{variable}} substitution. |
categories / custom_args | tags object | e.g. { category: 'receipts', order_id: '123' } |
| Unsubscribe groups (ASM) | Topics | Broadcasts can be scoped to a Topic. |
| Global unsubscribes / suppressions | Contacts with subscribed: false | |
| Marketing Campaigns single sends | Broadcasts | |
| Event Webhook | Webhooks (type transactional) |
How to migrate from SendGrid
- 01
Create an API key
Create a key under API keys in the dashboard. Keys start with
et_live_and are scoped to one domain workspace. Store it asEMAILR_API_KEYin your secret manager or environment, never in client-side code. - 02
Add and verify your sending domain
In the dashboard, open Domains and add the domain you send from. A subdomain such as
mail.yourcompany.comkeeps transactional reputation separate from your corporate mailbox.Emailr generates the DNS records: a DKIM TXT record on the
emailr._domainkeyselector, an SPF TXT record, and an MX + TXT pair on thesendsubdomain used as the MAIL FROM / Return-Path. A DMARC record is optional but recommended.SendGrid authenticates with CNAMEs such as
s1._domainkey; keep them in place until you've fully cut over.Emailr's DKIM selector and
sendsubdomain don't collide with other providers' records, so you can add them before cutting over and send from both providers during the migration. A domain can only have one SPF record: if one already exists, merge Emailr'sinclude:into it instead of adding a second record. - 03
Swap the SDK
Install the SDK and replace
sgMail.send()calls. The payload is close:to,from,subject,html,text,ccandbcckeep their names.replyTobecomesreply_to_email, andcategories/custom_argsbecome a singletagsobject.Attachments use
{ filename, content, contentType }with base64content, the same shape SendGrid uses minusdisposition.Terminalbashnpm install emailr - 04
Move dynamic templates
Recreate each dynamic template in emailr_ and send it with
template_idandtemplate_datainstead oftemplateIdanddynamicTemplateData.Emailr templates substitute flat
{{variable}}tokens fromtemplate_data. There are no loops or conditionals, so pre-render lists and optional blocks in your code and pass the result as a variable. - 05
Import suppressions
Export global unsubscribes, bounces and spam reports from SendGrid and import them as contacts with
subscribed: false. Broadcasts skip globally unsubscribed contacts, and Topic-scoped broadcasts also skip contacts who left that Topic. - 06
Recreate your event webhooks
Point a new emailr_ webhook at the same handler you use for SendGrid's Event Webhook, then translate event names with the table above. SendGrid posts batches of events; emailr_ posts one event per request.
Create a webhook of type
transactional(delivery and engagement events),domain(verification status) orreceiving(inbound email). Each webhook has its own signing secret. Every request carries anX-Emailr-Signatureheader: a hex-encoded HMAC-SHA256 of the raw request body. Verify it before trusting the payload.verify-webhook.tstsimport { createHmac, timingSafeEqual } from 'node:crypto' // rawBody must be the exact request body string, before JSON.parse export function isValidEmailrWebhook(rawBody: string, signature: string | null) { if (!signature) return false const expected = createHmac('sha256', process.env.EMAILR_WEBHOOK_SECRET!) .update(rawBody) .digest('hex') const a = Buffer.from(expected) const b = Buffer.from(signature) return a.length === b.length && timingSafeEqual(a, b) } // const ok = isValidEmailrWebhook(body, req.headers.get('x-emailr-signature')) // const { event, data } = JSON.parse(body) // e.g. event === 'email.bounced' - 07
Cut over gradually
Route one message type (for example password resets) through emailr_ first, watch deliveries and bounces in the dashboard, then move the rest. Remove SendGrid's DNS records only after the last message has gone out through emailr_.
Webhook event mapping
emailr_ webhooks post one JSON event per request with an event name and a data object. Translate event names in your handler with this table.
| SendGrid event | emailr_ event |
|---|---|
| processed | email.sent |
| delivered | email.delivered |
| bounce | email.bounced |
| dropped | email.failed |
| spamreport | email.complained |
| open | email.opened |
| click | email.clicked |
Things to watch for
- SendGrid's
personalizationslets one call send different variables to many recipients. In emailr_, send one request per recipient, or use Broadcasts for list sends. - Handlebars helpers like
{{#each}}and{{#if}}don't exist in emailr_ templates; render that markup in code. - Click tracking rewrites links, so send a few test emails after cutover and check any URL allowlists or link checks in your app.
Frequently asked questions
How long does a SendGrid migration take?
The code change is usually a few lines per call site. Most of the time goes into DNS propagation for your domain and recreating templates. Running both providers in parallel means there is no hard deadline.
Can I keep sending through SendGrid while I migrate?
Yes. Emailr's DKIM selector and MAIL FROM subdomain don't collide with SendGrid's CNAME records, so both can be verified on the same domain at once. If your root domain already has an SPF record, merge the includes into one record.
Do SendGrid dynamic templates work as-is?
No. Recreate them as emailr_ templates. Simple `{{variable}}` placeholders carry over; Handlebars blocks such as each and if need to be rendered in your code first.
What happens to my suppression list?
Export it from SendGrid and import those addresses as contacts with subscribed set to false. Broadcasts automatically skip globally unsubscribed contacts.
Is there an equivalent of the SendGrid Event Webhook?
Yes. Create a transactional webhook to receive email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked and email.failed events, signed with an HMAC-SHA256 signature.
Keep going
Documentation
Related guides
- Migrate from MailgunSwitch from Mailgun to emailr_: replace mailgun.js, map tags, variables, templates and webhook events, move suppressions and cut over without downtime.
- Migrate from PostmarkMove from Postmark to emailr_: map servers, message streams, templates, tags and metadata, translate webhooks and import suppressions step by step.
- Migrate from Amazon SESReplace raw Amazon SES with emailr_: keep SES-grade delivery, drop SNS plumbing and configuration sets, and get templates, webhooks and an inbox.
Ready to leave SendGrid?
Create a free account, verify your domain and send your first email through emailr_ in minutes. No credit card required.