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 EU end-to-end: EU infrastructure, EU 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 <hello@acme.com>',
to: ['user@example.com'],
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 <hello@acme.com>',
to: 'user@example.com',
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 keep those addresses out of your sends.
- 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, SPF, 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.