# Migrate from Mailgun

> Move from Mailgun to Emailit. Map domains, keys and routes, convert form-encoded API calls to JSON, and move SMTP, webhooks, suppressions and templates.

This guide maps Mailgun concepts, API calls, webhooks, suppressions and templates to their Emailit equivalents. Read [Migrate to Emailit](/docs/migrate/) first for the overall order and how to run both providers in parallel.

## Concepts

| Mailgun | Emailit |
| --- | --- |
| Account and subaccounts | Account and [workspaces](/docs/workspaces/). Each workspace has its own domains, keys, members and credits. |
| Domain, with its own API path `/v3/<domain>/…` | [Sending domain](/docs/domains/). There's one send endpoint, and Emailit picks the domain from the `from` address. |
| Private API key | **Full Access** [API key](/docs/developers/api-keys/) |
| Domain sending key | **Sending Only** API key restricted to one domain |
| SMTP credentials per domain | Your API key, used as the SMTP password |
| Templates per domain, with versions | [Templates](/docs/templates/) per workspace, with an alias and versions |
| Webhooks per domain | [Webhooks](/docs/webhooks/) per workspace |
| Routes | [Inbound email](/docs/inbound/) with the `email.received` webhook, or the **Forward received email** [automation](/docs/inbound/forward-with-automations/) |
| Suppressions per domain: bounces, unsubscribes, complaints | One [suppression list](/docs/suppressions/) per workspace |
| Mailing lists | [Audiences](/docs/audiences/) |
| Tags and custom variables | `meta` |
| Logs and events | **Email API → Emails**, **Email API → Events** and **Email API → Logs** |
| Email validation | [Email verification](/docs/email-verification/) |

## Update your API calls

Mailgun's `POST /v3/<domain>/messages` takes form fields with basic authentication. Emailit's `POST /v2/emails` takes JSON with a bearer token:

```bash title="Before: Mailgun"
curl -s --user "api:$MAILGUN_API_KEY" \
  https://api.mailgun.net/v3/mg.acme.com/messages \
  -F from='Acme <hello@mg.acme.com>' \
  -F to='ada@example.com' \
  -F subject='Your receipt' \
  -F text='Thanks for your order.' \
  --form-string html='<p>Thanks for your order.</p>'
```

```bash title="After: Emailit"
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@mg.acme.com>",
    "to": "ada@example.com",
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "html": "<p>Thanks for your order.</p>"
  }'
```

| Mailgun | Emailit |
| --- | --- |
| Basic auth `api:<key>` | `Authorization: Bearer secret_…` |
| `multipart/form-data` fields | A JSON body |
| `from`, `subject`, `text`, `html` | The same names |
| `to`, `cc`, `bcc` (repeated or comma-separated) | `to`, `cc`, `bcc` as a string or an array of up to 50 each |
| `h:Reply-To` | `reply_to` |
| `h:X-My-Header` | `headers: { "X-My-Header": "…" }` |
| `v:order-id`, `h:X-Mailgun-Variables` | `meta: { "order-id": "…" }`, returned in webhook events |
| `template` and `t:variables` | `template` (an ID or alias) and `variables` |
| `attachment`, `inline` (file uploads) | `attachments[]` with base64 `content` or a `url`, plus `content_type`. Add `content_id` for inline images. |
| `o:deliverytime` (RFC 2822 date) | `scheduled_at` (ISO 8601, Unix time or plain English) |
| `o:tracking`, `o:tracking-opens`, `o:tracking-clicks` | `tracking: { "loads": true, "clicks": true }` |
| `o:tag` | `meta` |
| `o:testmode` | Not available |
| `recipient-variables` (batch sending) | Not available. Send one request per recipient with its own `variables`. |
| Response `{ "id": "<…>", "message": "Queued. Thank you." }` | `200` with `id` (`em_…`), `message_id`, `status: "accepted"` and `ids` per recipient |

If you sent from a subdomain such as `mg.acme.com`, add that exact subdomain in Emailit. Subdomains are verified separately from the parent domain. Mailgun's EU and US API hosts both map to the single Emailit endpoint. See [Send an email](/docs/email-api/send-email/).

## Switch SMTP settings

| Setting | Mailgun | Emailit |
| --- | --- | --- |
| Host | `smtp.mailgun.org`, or the EU host | `smtp.emailit.com` |
| Port | `587`, `465`, `2525` or `25` | `587` (STARTTLS), `465` (TLS), `2525`, `2587` or `25` |
| Username | Your SMTP login, such as `postmaster@mg.acme.com` | `emailit` |
| Password | Your SMTP password | Your Emailit API key |

Emailit doesn't read `X-Mailgun-*` headers. Remove them, and set tracking on the domain instead. See [SMTP settings](/docs/smtp/settings/).

## Map webhook events

| Mailgun event | Emailit event |
| --- | --- |
| `accepted` | `email.accepted` (API only) |
| `delivered` | `email.delivered` |
| `failed` with severity `temporary` | `email.attempted` |
| `failed` with severity `permanent` | `email.bounced` |
| `opened` | `email.loaded` |
| `clicked` | `email.clicked` |
| `complained` | `email.complained` |
| `unsubscribed` | `email.unsubscribed`, for campaign email only |
| Route that forwards to a URL | `email.received`, then fetch the content with [`GET /emails/{id}`](/docs/api-reference/emails/get/) |

The request format changes:

- Mailgun posts one event per request, with the details in `event-data`. Emailit posts a JSON array of up to 100 events. Loop over the array.
- The event name is in `type`, and the email is in `data.object`. Use `data.object.id`, the `em_` ID from the send response, to match events to messages. Your `meta` values are in `data.object.meta`.
- Mailgun signs a timestamp and token inside the body. Emailit signs the whole raw body: verify `X-Emailit-Signature` against `X-Emailit-Timestamp` and your `whsec_` secret. See [Request signature](/docs/webhooks/request-signature/).

```javascript
for (const event of req.body) {
  const email = event.data.object;
  if (event.type === 'email.bounced') markBounced(email.to, email.id);
  if (event.type === 'email.complained') unsubscribe(email.to);
}
```

## Move suppressions

1. Export the **Bounces**, **Complaints** and **Unsubscribes** lists of every Mailgun domain you send from, from the control panel or with the suppressions API (`/v3/<domain>/bounces`, `/complaints` and `/unsubscribes`).

2. Build one CSV with the columns `email,type,reason`:

```csv
email,type,reason
old-address@example.com,recipient,mailgun bounce
angry@example.com,recipient,mailgun complaint
```

   Use the type `recipient` for addresses that must never receive email. It blocks API, SMTP and campaign sends. The types `bounce`, `complaint` and `unsubscribe` only stop campaigns.

3. In **Email API → Suppressions**, select **Import** and upload the file. Each file can have up to 10,000 rows and can be at most 8 MB. Duplicates are skipped.

Emailit has one suppression list per workspace, so addresses from all your Mailgun domains go into the same list. There's no allowlist. See [Manage suppressions](/docs/suppressions/manage/).

## Move templates

Copy each template's HTML from Mailgun, then import it in **Email Marketing → Templates** or create it with the [Templates API](/docs/api-reference/templates/create/). Give it an alias and send it with `"template": "<alias>"` and `variables`.

Mailgun templates use Handlebars. Temple covers the common parts:

| Mailgun (Handlebars) | Emailit (Temple) |
| --- | --- |
| `{{first_name}}` | `{{first_name}}` |
| `{{{html_block}}}` | `{{html_block}}`. Temple never escapes HTML, so escape user input yourself. |
| `{{#if plan}}…{{else}}…{{/if}}` | The same |
| `{{#unless plan}}…{{/unless}}` | `{{#if plan}}{{else}}…{{/if}}` |
| `{{#each items}}…{{/each}}` | Not supported. Render the list in your code and pass it as one variable. |
| `{{#equal plan "pro"}}…{{/equal}}` | Not supported. Pass a boolean such as `is_pro` and use `{{#if is_pro}}`. |
| No built-in default | `{{first_name\|"there"}}` adds a fallback |

See [Temple](/docs/templates/temple/) and [Import and export templates](/docs/templates/import-export/).

## Change DNS

Add each domain in **Email API → Domains** and publish the Emailit records. They use their own names (`emailit._domainkey`, `emailit.<domain>`, and optionally `go` and `inbound`), so they don't conflict with Mailgun's DKIM record or its `email.<domain>` tracking CNAME. You don't need to change your root SPF record for Emailit. Keep your DMARC record. See [DNS records](/docs/domains/dns-records/).

After the cutover, remove Mailgun's DKIM and tracking records, and remove `include:mailgun.org` from your SPF record. If you receive mail through Mailgun routes, keep its MX records until you've moved that traffic to [Emailit inbound](/docs/inbound/set-up/), which receives on a subdomain such as `inbound.acme.com`.

## Next steps

- [Go-live checklist](/docs/get-started/go-live/)
- [Set up webhooks](/docs/webhooks/set-up/)
- [Priority migration](/docs/programs/priority-migration/): let Emailit engineers do the move with you

---
Source: https://emailit.com/docs/migrate/mailgun/
