# Migrate from Mailgun

> Migrate from Mailgun without a rewrite: same POST /v3/:domain/messages endpoint, same api:key Basic auth, same {id, message} reply — new base URL and key.

Canonical: https://emailfast.dev/migrate/mailgun

## 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`:

```diff
  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:

```bash
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

```diff
- 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

- [Why teams leave Mailgun](/compare/mailgun-alternative)
- [The full migration kit](/features/migration)
- [API docs](/docs)

## 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:&lt;key&gt; , urlencoded or multipart bodies, and the familiar { &quot;id&quot;: …, &quot;message&quot;: &quot;Queued. Thank you.&quot; } 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.
