Migrate from Mailgun
Point your Mailgun client at a new base URL with a new key, or adopt our SDK. Your form-encoded sends keep working.
Facts verified 2026-07-17 — corrections: hello@emailfast.dev
Two ways to switch
Email Fast speaks Mailgun's send API natively: SendGrid-, Mailgun-, and Postmark-compatible endpoints: point your existing SDK at a new base URL with a new key and keep your code. Both body encodings Mailgun clients use — application/x-www-form-urlencoded and multipart/form-data — are accepted, and the reply is Mailgun-shaped: { "id": "<msg_…@yourdomain.com>", "message": "Queued. Thank you." }.
Path 1 — keep your Mailgun client
With mailgun.js, the switch is a key and a url:
import formData from "form-data";
import Mailgun from "mailgun.js";
const mg = new Mailgun(formData).client({
username: "api",
- key: process.env.MAILGUN_API_KEY,
+ key: process.env.EMAILFAST_API_KEY, // ef_sandbox_… works everywhere
+ url: "https://api.emailfast.dev",
});
await mg.messages.create("yourdomain.com", {
from: "You <you@yourdomain.com>",
to: "ada@example.com, grace@example.com", // comma fan-out, one message each
subject: "Welcome",
html: "<h1>Hi</h1>",
});Calling over raw HTTP instead? Add an Idempotency-Key header — retries become safe per recipient:
curl -s --user "api:$EMAILFAST_API_KEY" \
https://api.emailfast.dev/v3/yourdomain.com/messages \
-H "Idempotency-Key: welcome-batch-1" \
-F from="You <you@yourdomain.com>" \
-F to="ada@example.com, grace@example.com" \
-F subject="Welcome" \
-F html="<h1>Hi</h1>"Path 2 — adopt @email-fast/nodejs
- await mg.messages.create("yourdomain.com", {
- from: "You <you@yourdomain.com>",
- to: "ada@example.com",
- subject: "Welcome",
- html: "<h1>Hi</h1>",
- });
+ const ef = new EmailFast({ apiKey: process.env.EMAILFAST_API_KEY, baseUrl: "https://api.emailfast.dev" });
+ await ef.send({
+ to: "ada@example.com",
+ subject: "Welcome",
+ html: "<h1>Hi</h1>",
+ idempotency_key: "welcome-ada-1",
+ });What maps 1:1
Endpoint, auth, both encodings, comma-separated to fan-out, and the response shape. Every message admitted this way passes the same checkpoint as every other ingress: every send — REST, SMTP, browser SDK, compatibility endpoints, broadcasts, automations — passes through one admission gate: idempotency, suppression, quota, and content policy in a single checkpoint no ingress can skip.
What arms at launch
The hosted data importers — templates, contacts, and suppression lists pulled from your Mailgun account — arm at launch. Today you can evaluate the send path end to end with a sandbox key: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.
The idempotency bonus
Mailgun's send endpoint has no idempotency. Ours does: one Idempotency-Key header covers a comma-to fan-out with a derived key per recipient, so a timeout-and-retry can never double-send part of a batch: a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send.
Next
Questions, answered plainly
Do I need to change my application code to migrate from Mailgun?
No. POST /v3/:domain/messages is accepted as-is — HTTP Basic auth as api:<key>, urlencoded or multipart bodies, and the familiar { "id": …, "message": "Queued. Thank you." } response.
Does a comma-separated `to` still send to each recipient?
Yes. A comma-separated to fans out to one message per recipient, in both urlencoded and multipart form. If any recipient is blocked by policy, the whole call rolls back and returns a truthful error.
How do I test before pointing production at Email Fast?
Use a sandbox key (ef_sandbox_…) as the Basic-auth password. The full pipeline runs — validation, rendering, events, a hosted capture inbox — and no email leaves.