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
| Mailgun | EuroMail | |
|---|---|---|
| Endpoint | POST https://api.mailgun.net/v3/{your-domain}/messages | POST https://api.euromail.dev/v1/emails, no domain in the path |
| Body format | Form-encoded (multipart/form-data) | JSON |
| Auth | HTTP Basic: username api, password = API key | X-EuroMail-Api-Key: em_live_... header |
| HTML body | html (form field) | html_body (JSON field) |
| Text body | text | text_body |
| Attachments | attachment (multipart file upload) | attachments: [{ filename, content, content_type }] (base64 in JSON) |
| Scheduling | o: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 headers | h:X-My-Header (prefixed form field) | headers are not a general passthrough today. Use metadata for correlation data instead |
| Tagging | o:tag (up to 3 per message) | tags (array of strings, no fixed cap) |
| Variables | v:my-var (prefixed form field, echoed in webhooks) | metadata (single JSON object, echoed on webhooks) |
| Idempotency | None documented | idempotency_key body field. Same key always returns the original result |
| Success response | 200 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
| Setting | Mailgun | EuroMail |
|---|---|---|
| Host | smtp.mailgun.org (EU: smtp.eu.mailgun.org) | smtp.euromail.dev |
| Port | 587, 465, 25, or 2525 | 587 (STARTTLS) or 465 (implicit TLS) |
| Username | [email protected], per-domain, not your account login | apikey (literal string) |
| Password | your 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
- Add your domain in the EuroMail dashboard and publish its DKIM, SPF, and DMARC records. Domain Verification.
- Keep Mailgun's records in place during the dual-run. Different DKIM
selectors and stacked SPF
include:s let both providers verify at once. - Remove Mailgun's records after cutover, on your own schedule.
Webhook event mapping
| Mailgun event | EuroMail event |
|---|---|
delivered | delivered |
permanent_fail | bounced |
temporary_fail | deferred |
complained | complained |
opened | opened |
clicked | clicked |
unsubscribed | Handled 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
- Create a EuroMail account and an API key with the
emails:sendscope (API Keys). - Verify your sending domain; confirm DKIM/SPF/DMARC show green.
- Register your webhook endpoint and swap the signature verification.
- Import suppressions from your Mailgun export.
- Switch from form-encoded requests to JSON. Update the endpoint, auth
method (Basic → header), and field names (or the SMTP username/password),
accepting
202as success instead of200. - Send a staging email end-to-end; confirm the
deliveredwebhook arrives. - Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
- Keep Mailgun read-only for a rollback window, then retire it.