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.
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.
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 <hello@mail.acme.com>',
to: ['user@example.com'],
subject: 'Your receipt',
html: '<p>Thanks for your order.</p>',
'o:tag': ['receipts'],
})import { Emailr } from 'emailr'
const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! })
await emailr.emails.send({
from: 'Acme <hello@mail.acme.com>',
to: ['user@example.com'],
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_.
| Mailgun | emailr_ | Notes |
|---|---|---|
| Private API key | API key (et_live_ prefix) | |
| Sending domain in the request URL | Domain taken from from and the key's workspace | |
o:tag | tags object | |
v: custom variables / X-Mailgun-Variables | tags object | String key/value pairs. |
Templates (template + t:variables) | Templates (template_id + template_data) | |
o:deliverytime | scheduled_at (ISO 8601) | |
| Suppressions (bounces, unsubscribes, complaints) | Contacts with subscribed: false | |
| Routes (inbound) | Inboxes and receiving webhooks | |
| Webhooks | Webhooks (type transactional) |
How to migrate from Mailgun
- 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.Mailgun publishes its own DKIM selector and an
email.tracking CNAME; leave those alone during the transition.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
mg.messages.create(domain, data)withemailr.emails.send(data). Drop the domain argument.h:Reply-Tobecomesreply_to_email, ando:tagplusv:variables collapse into onetagsobject.Mailgun takes attachments as multipart file uploads. emailr_ takes JSON, so pass
{ filename, content, contentType }with base64content.Terminalbashnpm install emailr - 04
Rebuild templates
Recreate stored Mailgun templates in emailr_ and send them with
template_idandtemplate_data.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
Move suppressions
Export the bounces, unsubscribes and complaints lists from Mailgun and import those addresses as contacts with
subscribed: falseso broadcasts skip them. - 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) 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
Move inbound routes
If you use Mailgun Routes to receive mail, create an inbox in emailr_ and subscribe a
receivingwebhook toemail.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 event | emailr_ event |
|---|---|
| accepted | email.sent |
| delivered | email.delivered |
| failed (permanent) | email.bounced |
| complained | email.complained |
| opened | email.opened |
| clicked | email.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:testmodehas 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.
Keep going
Documentation
Related guides
- Migrate from SendGridMove from SendGrid to emailr_: map dynamic templates, categories, suppressions and Event Webhook events, swap the SDK and cut over with zero downtime.
- Migrate from SparkPostMove from SparkPost to emailr_: replace transmissions, map substitution data, campaigns, metadata, suppressions and webhook events step by step.
- Migrate from PostmarkMove from Postmark to emailr_: map servers, message streams, templates, tags and metadata, translate webhooks and import suppressions step by step.
Ready to leave Mailgun?
Create a free account, verify your domain and send your first email through emailr_ in minutes. No credit card required.