Migrate from Mailgun
Mailgun's EU region keeps your mail data in Europe, but the company operating it is still US-owned — which matters if EU data handling is why you chose the EU region in the first place. eusend is European end-to-end: EU infrastructure, Norwegian company, no US parent. The migration itself is small; the send call gets simpler, since eusend has no per-domain API endpoints.
The code change
Install the SDK with npm install @eusend_dev/sdk. Two structural differences: there's no domain argument — the domain comes from your from address, which must be verified — and no form-data plumbing, since the API is plain JSON. Calls return { data, error } instead of throwing.
import formData from 'form-data'
import Mailgun from 'mailgun.js'
const mg = new Mailgun(formData).client({
username: 'api',
key: process.env.MAILGUN_API_KEY,
url: 'https://api.eu.mailgun.net',
})
await mg.messages.create('mg.acme.com', {
from: 'Acme <[email protected]>',
to: ['[email protected]'],
subject: 'Welcome',
html: '<p>Thanks for signing up.</p>',
})import { Eusend } from '@eusend_dev/sdk'
const eusend = new Eusend(
process.env.EUSEND_API_KEY
)
await eusend.emails.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
subject: 'Welcome',
html: '<p>Thanks for signing up.</p>',
})Field mapping
| Mailgun | eusend | Notes |
|---|---|---|
| domain argument | — | Dropped. The from address determines the domain; verify it in eusend first. |
| from / to / cc / bcc / subject / html / text | Same names | to accepts a string or an array |
| h:Reply-To | replyTo | A real field instead of an h: header |
| h:X-* headers | headers | A plain object; no h: prefix convention |
| o:tracking-opens / o:tracking-clicks | trackOpens / trackClicks | Both default to true on eusend |
| o:tag | — | No direct equivalent yet. Custom headers are the usual workaround. |
| recipient-variables | batch.send([...]) | One fully-rendered message per recipient, up to 100 per request |
If you send over SMTP
Mailgun's per-domain SMTP credentials become one credential: username eusend, your API key as the password. Use port 465 with implicit TLS — eusend does not support STARTTLS on 587.
// Before: smtp.mailgun.org (or smtp.eu.mailgun.org), postmaster@ user
// After — one endpoint, fixed username, API key as password, implicit TLS on 465
const transport = nodemailer.createTransport({
host: 'smtp.eusend.dev',
port: 465,
secure: true,
auth: { user: 'eusend', pass: process.env.EUSEND_API_KEY },
})Webhooks
The event model is the same — signed HTTP posts per event — so mostly you're renaming things:
| Mailgun event | eusend event |
|---|---|
| accepted | email.sent |
| delivered | email.delivered |
| permanent_fail | email.bounced |
| complained | email.complained |
| opened | email.opened |
| clicked | email.clicked |
Both providers sign with HMAC-SHA256; only the payload layout differs. Note that Mailgun's temporary_fail has no eusend event — retries on soft failures happen inside the platform, and you hear about the final outcome. The webhook docs show the payload and signing scheme.
What's honestly different
- Attachments work, just as JSON. Mailgun takes attachments as multipart file uploads; eusend takes them inline on the JSON body — base64-encode the bytes into
attachments[].contentwith afilename(and optionalcontent_type/content_idfor inline images). Up to 20 files, 10 MB combined per message. - Bring your suppression list. eusend suppresses hard bounces and complaints automatically going forward, but it can't see your Mailgun history. Export your suppressions and import the CSV before your first send, so addresses that already bounced don't get a fresh attempt from a new IP.
- Mailing-list campaigns wait. Transactional sending is open today; bulk campaigns open as the platform warms. If you use Mailgun mailing lists for marketing sends, plan that part for later.
Before you flip production
- Verify your sending domain and add its DKIM and DMARC records — the Domains page generates them. If you sent from a dedicated subdomain like
mg.acme.com, you can keep it or take the move as the moment to switch to a cleaner one. - Migrate against an
eu_test_API key first — test sends are accepted and tracked but never delivered — then switch toeu_live_. - Keep Mailgun alive for a week or two after cutover so in-flight events and your suppression export have somewhere to come from.