New from emailrHealthCheck Email iconHealthCheck Email
emailr_

Migrate from Mailgun to emailr_

Switch from Mailgun to emailr_: replace mailgun.js, map tags, variables, templates and webhook events, move suppressions and cut over without downtime.

Mailgun iconMailgunemailr_

This guide replaces a Mailgun integration with emailr_. You'll swap mailgun.js for the emailr SDK, convert o:tag and v: variables to tags, rebuild templates and webhooks, and import your suppressions.

Mailgun scopes every API call to a sending domain in the URL. In emailr_ the domain comes from the from address and the API key's workspace, so the call sites get simpler.

The code change

A typical Mailgun send call next to its emailr_ equivalent.

Before (Mailgun)ts
import FormData from 'form-data'
import Mailgun from 'mailgun.js'

const mailgun = new Mailgun(FormData)
const mg = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! })

await mg.messages.create('mail.acme.com', {
  from: 'Acme <[email protected]>',
  to: ['[email protected]'],
  subject: 'Your receipt',
  html: '<p>Thanks for your order.</p>',
  'o:tag': ['receipts'],
})
After (emailr_)ts
import { Emailr } from 'emailr'

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

await emailr.emails.send({
  from: 'Acme <[email protected]>',
  to: ['[email protected]'],
  subject: 'Your receipt',
  html: '<p>Thanks for your order.</p>',
  tags: { category: 'receipts' },
})

Mailgun concepts in emailr_

Use this table to find where each feature you rely on lives in emailr_.

Mailgunemailr_Notes
Private API keyAPI key (et_live_ prefix)
Sending domain in the request URLDomain taken from from and the key's workspace
o:tagtags object
v: custom variables / X-Mailgun-Variablestags objectString key/value pairs.
Templates (template + t:variables)Templates (template_id + template_data)
o:deliverytimescheduled_at (ISO 8601)
Suppressions (bounces, unsubscribes, complaints)Contacts with subscribed: false
Routes (inbound)Inboxes and receiving webhooks
WebhooksWebhooks (type transactional)

How to migrate from Mailgun

  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.

    Mailgun publishes its own DKIM selector and an email. tracking CNAME; leave those alone during the transition.

    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 mg.messages.create(domain, data) with emailr.emails.send(data). Drop the domain argument. h:Reply-To becomes reply_to_email, and o:tag plus v: variables collapse into one tags object.

    Mailgun takes attachments as multipart file uploads. emailr_ takes JSON, so pass { filename, content, contentType } with base64 content.

    Terminalbash
    npm install emailr
  4. 04

    Rebuild templates

    Recreate stored Mailgun templates in emailr_ and send them with template_id and template_data.

    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

    Move suppressions

    Export the bounces, unsubscribes and complaints lists from Mailgun and import those addresses as contacts with subscribed: false so broadcasts skip them.

  6. 06

    Recreate your event webhooks

    Mailgun signs webhooks with a timestamp, token and signature in the body. emailr_ signs the raw body instead and sends the signature in a header, so the verification code changes but the event handling can stay.

    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

    Move inbound routes

    If you use Mailgun Routes to receive mail, create an inbox in emailr_ and subscribe a receiving webhook to email.received. Inbound messages are threaded in the emailr_ inbox, so replies stay attached to the conversation.

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.

Mailgun eventemailr_ event
acceptedemail.sent
deliveredemail.delivered
failed (permanent)email.bounced
complainedemail.complained
openedemail.opened
clickedemail.clicked

Things to watch for

  • Mailgun recipient variables (batch sending with %recipient.name%) have no direct equivalent; loop over recipients or use Broadcasts.
  • If you have EU and US Mailgun regions, note that emailr_ uses a single API base URL: https://api.emailr.dev.
  • Mailgun's o:testmode has no counterpart, so send test traffic to addresses you control.

Frequently asked questions

Do I need to change my sending domain to move off Mailgun?

No. Add the same domain or subdomain to emailr_ and publish its DKIM, SPF and MAIL FROM records. They sit alongside Mailgun's records, so you can migrate without changing the from address.

How do Mailgun tags and variables map to emailr_?

Both become the tags object on the send request, for example tags: { category: 'receipts', order_id: '123' }. Tags appear on the email record and in your logs.

Can emailr_ receive email like Mailgun Routes?

Yes. Create an inbox for the address and subscribe a receiving webhook to email.received, or work with the threaded conversation in the emailr_ inbox directly.

How do I verify emailr_ webhooks?

Compute an HMAC-SHA256 of the raw request body with the webhook's signing secret and compare it to the hex value in the X-Emailr-Signature header.

Ready to leave Mailgun?

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