← Back to docs

Migrate from Mailgun

Move transactional email from Mailgun to EuroMail: API field mapping, code before/after, SMTP cutover, DNS, webhooks, and a zero-downtime checklist.

What changes, what doesn't

Both are per-domain, developer-first sending APIs with webhooks for delivery events, so the integration shape survives. The concrete change with the widest blast radius: Mailgun's HTTP API is form-encoded (multipart/form-data or application/x-www-form-urlencoded), while EuroMail's is JSON. If your client builds a FormData object today, that code needs to change, not just the field names. The honest comparison, including where Mailgun is the better choice, is at euromail vs Mailgun.

Verify your domain on EuroMail first, run both in parallel, then cut over.

API mapping

MailgunEuroMail
EndpointPOST https://api.mailgun.net/v3/{your-domain}/messagesPOST https://api.euromail.dev/v1/emails, no domain in the path
Body formatForm-encoded (multipart/form-data)JSON
AuthHTTP Basic: username api, password = API keyX-EuroMail-Api-Key: em_live_... header
HTML bodyhtml (form field)html_body (JSON field)
Text bodytexttext_body
Attachmentsattachment (multipart file upload)attachments: [{ filename, content, content_type }] (base64 in JSON)
Schedulingo:deliverytime (RFC 2822 format, e.g. Fri, 14 Oct 2026 12:00:00 +0000)send_at (RFC 3339, e.g. 2026-10-14T12:00:00Z)
Custom headersh:X-My-Header (prefixed form field)headers are not a general passthrough today. Use metadata for correlation data instead
Taggingo:tag (up to 3 per message)tags (array of strings, no fixed cap)
Variablesv:my-var (prefixed form field, echoed in webhooks)metadata (single JSON object, echoed on webhooks)
IdempotencyNone documentedidempotency_key body field. Same key always returns the original result
Success response200 with { "id", "message": "Queued. Thank you." }202 Accepted with { "id", "status": "queued", ... }

The endpoint itself simplifies: no per-domain path segment, because your API key is already scoped to your account and the from address determines the sending domain (which must be verified first).

Code before and after

curl:

# Before: Mailgun (form-encoded, HTTP Basic auth)
curl -X POST https://api.mailgun.net/v3/yourdomain.com/messages \
  -u "api:your-mailgun-api-key" \
  -F from="[email protected]" \
  -F to="[email protected]" \
  -F subject="Welcome aboard" \
  -F html="<h1>Welcome!</h1>"

# After: EuroMail (JSON, header auth)
curl -X POST https://api.euromail.dev/v1/emails \
  -H "X-EuroMail-Api-Key: em_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Welcome aboard",
    "html_body": "<h1>Welcome!</h1>"
  }'

TypeScript:

// Before: Mailgun
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("yourdomain.com", {
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome aboard",
  html: "<h1>Welcome!</h1>",
});

// After: EuroMail
import { EuroMail } from "@euromail/sdk";
const client = new EuroMail({ apiKey: process.env.EUROMAIL_API_KEY! });

await client.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome aboard",
  html_body: "<h1>Welcome!</h1>",
});

The SDK swap removes the form-encoding concern entirely. Python, Rust, and Go SDKs all send JSON with the same field names.

SMTP cutover

SettingMailgunEuroMail
Hostsmtp.mailgun.org (EU: smtp.eu.mailgun.org)smtp.euromail.dev
Port587, 465, 25, or 2525587 (STARTTLS) or 465 (implicit TLS)
Username[email protected], per-domain, not your account loginapikey (literal string)
Passwordyour domain's SMTP password (dashboard → domain settings)your EuroMail API key (em_live_...)

The username convention changes the most: Mailgun issues a separate SMTP password per sending domain, while EuroMail's SMTP relay authenticates with your account-level API key under the literal username apikey. The same convention SendGrid, Postmark, and SES SMTP presets use, so most "SMTP provider" dropdowns work unchanged. Full walkthroughs in Send Email via SMTP.

DNS: verify your domain before switching

  1. Add your domain in the EuroMail dashboard and publish its DKIM, SPF, and DMARC records. Domain Verification.
  2. Keep Mailgun's records in place during the dual-run. Different DKIM selectors and stacked SPF include:s let both providers verify at once.
  3. Remove Mailgun's records after cutover, on your own schedule.

Webhook event mapping

Mailgun eventEuroMail event
delivereddelivered
permanent_failbounced
temporary_faildeferred
complainedcomplained
openedopened
clickedclicked
unsubscribedHandled via suppression lists, not a webhook event

Signature verification changes: Mailgun signs with HMAC-SHA256 over a concatenated timestamp + token, delivered as three separate payload fields you combine yourself. EuroMail signs the whole payload once with HMAC-SHA256 in the X-EuroMail-Signature header. One hash instead of reconstructing the signed string. See Webhooks.

Bring your suppression list

Export bounces, complaints, and unsubscribes from the Mailgun dashboard, then replay them:

curl -X POST https://api.euromail.dev/v1/suppressions \
  -H "X-EuroMail-Api-Key: em_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email_address": "[email protected]",
    "reason": "migrated from Mailgun: hard bounce"
  }'

Details in Suppression Lists.

Honest gaps

  • Mailing lists don't port as-is. Mailgun's mailing-list feature ([email protected] expansion) has no direct equivalent. Recipient fan-out belongs in your application, or in Contact Lists + Newsletters for marketing sends.
  • Route-based inbound rules are structured differently. Mailgun's regex-match routing maps to EuroMail's inbound routes, but the rule syntax isn't a drop-in port. Expect to recreate routing logic, not copy-paste it.
  • Full comparison at euromail vs Mailgun.

Cutover checklist

  1. Create a EuroMail account and an API key with the emails:send scope (API Keys).
  2. Verify your sending domain; confirm DKIM/SPF/DMARC show green.
  3. Register your webhook endpoint and swap the signature verification.
  4. Import suppressions from your Mailgun export.
  5. Switch from form-encoded requests to JSON. Update the endpoint, auth method (Basic → header), and field names (or the SMTP username/password), accepting 202 as success instead of 200.
  6. Send a staging email end-to-end; confirm the delivered webhook arrives.
  7. Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
  8. Keep Mailgun read-only for a rollback window, then retire it.

Send your first email in about 90 seconds

The free tier includes 1,000 emails a month. All data stays in Finland | GDPR compliance without the paperwork.

Create free account See live delivery data