Migration

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.

Before · Mailgun
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>',
})
After · eusend
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

MailguneusendNotes
domain argumentDropped. The from address determines the domain; verify it in eusend first.
from / to / cc / bcc / subject / html / textSame namesto accepts a string or an array
h:Reply-ToreplyToA real field instead of an h: header
h:X-* headersheadersA plain object; no h: prefix convention
o:tracking-opens / o:tracking-clickstrackOpens / trackClicksBoth default to true on eusend
o:tagNo direct equivalent yet. Custom headers are the usual workaround.
recipient-variablesbatch.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.

nodemailer
// 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 eventeusend event
acceptedemail.sent
deliveredemail.delivered
permanent_failemail.bounced
complainedemail.complained
openedemail.opened
clickedemail.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

Before you flip production

Want it done for you?
The migration page has a copy-paste prompt that lets an AI coding agent do the whole swap against our live API reference. Or email support@eusend.dev — we're happy to help you move over.