New from emailrHealthCheck Email iconHealthCheck Email
emailr_

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.

SendGrid iconSendGridemailr_

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.

Before (SendGrid)ts
import sgMail from '@sendgrid/mail'

sgMail.setApiKey(process.env.SENDGRID_API_KEY!)

await sgMail.send({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Your receipt',
  html: '<p>Thanks for your order.</p>',
  categories: ['receipts'],
})
After (emailr_)ts
import { Emailr } from 'emailr'

const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! })

await emailr.emails.send({
  to: '[email protected]',
  from: '[email protected]',
  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_.

SendGridemailr_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_argstags objecte.g. { category: 'receipts', order_id: '123' }
Unsubscribe groups (ASM)TopicsBroadcasts can be scoped to a Topic.
Global unsubscribes / suppressionsContacts with subscribed: false
Marketing Campaigns single sendsBroadcasts
Event WebhookWebhooks (type transactional)

How to migrate from SendGrid

  1. 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 as EMAILR_API_KEY in your secret manager or environment, never in client-side code.

  2. 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.com keeps transactional reputation separate from your corporate mailbox.

    Emailr generates the DNS records: a DKIM TXT record on the emailr._domainkey selector, an SPF TXT record, and an MX + TXT pair on the send subdomain 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 send subdomain 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's include: into it instead of adding a second record.

  3. 03

    Swap the SDK

    Install the SDK and replace sgMail.send() calls. The payload is close: to, from, subject, html, text, cc and bcc keep their names. replyTo becomes reply_to_email, and categories / custom_args become a single tags object.

    Attachments use { filename, content, contentType } with base64 content, the same shape SendGrid uses minus disposition.

    Terminalbash
    npm install emailr
  4. 04

    Move dynamic templates

    Recreate each dynamic template in emailr_ and send it with template_id and template_data instead of templateId and dynamicTemplateData.

    Emailr templates substitute flat {{variable}} tokens from template_data. There are no loops or conditionals, so pre-render lists and optional blocks in your code and pass the result as a variable.

  5. 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.

  6. 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) or receiving (inbound email). Each webhook has its own signing secret. Every request carries an X-Emailr-Signature header: a hex-encoded HMAC-SHA256 of the raw request body. Verify it before trusting the payload.

    verify-webhook.tsts
    import { 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'
  7. 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 eventemailr_ event
processedemail.sent
deliveredemail.delivered
bounceemail.bounced
droppedemail.failed
spamreportemail.complained
openemail.opened
clickemail.clicked

Things to watch for

  • SendGrid's personalizations lets 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.

Ready to leave SendGrid?

Create a free account, verify your domain and send your first email through emailr_ in minutes. No credit card required.