> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from Mailgun

> How to migrate from Mailgun to Mailofly: replacing legacy multipart form-data requests with clean JSON APIs and modern webhooks.

# Migrating from Mailgun to Mailofly

Mailgun has been a legacy email provider for over a decade, but modern teams often find its `multipart/form-data` API design awkward, its domain-scoped URL paths clunky (`/v3/DOMAIN_NAME/messages`), and its billing tiers unpredictable.

Mailofly simplifies email sending with clean REST JSON endpoints, typed modern SDKs, predictable pricing, and instant real-time logs.

***

## Key Differences at a Glance

| Aspect | Mailgun | Mailofly |
| :- | :- | :- |
| **API Format** | `multipart/form-data` or URL-encoded | Clean JSON (`application/json`) |
| **Endpoint URL** | Domain-dependent (`/v3/{domain}/messages`) | Uniform URL (`https://api.mailofly.com/emails`) |
| **Authentication** | HTTP Basic Auth (`api:YOUR_API_KEY`) | Standard Bearer Token (`Bearer mf_live_...`) |
| **Subaccounts** | Subaccounts requiring dedicated setup | Sending Identities with `account_key` |
| **AI Integration** | None | Official `@mailofly/mcp` Server |

***

## 1. Code Comparison

### Node.js / TypeScript

```ts Before: Mailgun (mailgun.js) theme={null}
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!,
});

const response = await mg.messages.create("mg.yourdomain.com", {
  from: "Excited User <mailgun@yourdomain.com>",
  to: ["test@example.com"],
  subject: "Hello from Mailgun",
  text: "Testing some Mailgun awesomeness!",
  html: "<h1>Testing some Mailgun awesomeness!</h1>",
});

console.log("Mailgun ID:", response.id);
```

```ts After: Mailofly (@mailofly/node) theme={null}
import { Mailofly } from "@mailofly/node";

const mailofly = new Mailofly({
  apiKey: process.env.MAILOFLY_API_KEY!,
});

const { id } = await mailofly.emails.send({
  from: "Acme <hello@yourdomain.com>",
  to: ["test@example.com"],
  subject: "Hello from Mailofly",
  text: "Testing clean email delivery!",
  html: "<h1>Testing clean email delivery!</h1>",
});

console.log("Mailofly ID:", id);
```

***

### Python

```python Before: Mailgun (requests) theme={null}
import requests
import os

response = requests.post(
    "https://api.mailgun.net/v3/yourdomain.com/messages",
    auth=("api", os.environ["MAILGUN_API_KEY"]),
    data={
        "from": "Acme <hello@yourdomain.com>",
        "to": ["test@example.com"],
        "subject": "Hello",
        "text": "Testing Mailgun",
    },
)
```

```python After: Mailofly theme={null}
from mailofly import Mailofly
import os

client = Mailofly(api_key=os.environ["MAILOFLY_API_KEY"])

response = client.emails.send(
    sender="Acme <hello@yourdomain.com>",
    to=["test@example.com"],
    subject="Hello",
    text="Testing Mailofly",
)
```

***

### cURL

```bash Before: Mailgun (Form-Data & Basic Auth) theme={null}
curl -s --user 'api:YOUR_MAILGUN_KEY' \
  https://api.mailgun.net/v3/yourdomain.com/messages \
  -F from='Excited User <mailgun@yourdomain.com>' \
  -F to=test@example.com \
  -F subject='Hello' \
  -F text='Testing Mailgun'
```

```bash After: Mailofly (Standard JSON & Bearer Token) theme={null}
curl -X POST https://api.mailofly.com/emails \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Excited User <mailgun@yourdomain.com>",
    "to": ["test@example.com"],
    "subject": "Hello",
    "text": "Testing Mailofly"
  }'
```

***

## 2. Migrating Suppressions (Bounces & Complaints)

Mailgun tracks three suppression lists:

1. **Bounces**: `/v3/{domain}/bounces`
2. **Unsubscribes**: `/v3/{domain}/unsubscribes`
3. **Complaints**: `/v3/{domain}/complaints`

### Export from Mailgun

1. Log in to the Mailgun control panel.
2. Go to **Sending → Suppressions**.
3. Select your domain and download the CSVs for Bounces, Unsubscribes, and Complaints.

### Import into Mailofly

1. In Mailofly, navigate to **[Audiences > Contacts](https://www.mailofly.com/user/audience)**.
2. Click **Import CSV**.
3. Select your suppression files and check **Mark as Suppressed / Unsubscribed**.
4. Mailofly will ensure no further emails are dispatched to these addresses.

***

## 3. Webhook Migration

Mailgun requires calculating an HMAC-SHA256 digest on `timestamp + token` using your Mailgun signing key. Mailofly uses standard Svix / HMAC webhooks.

### Event Name Mapping

| Mailgun Event | Mailofly Event | Description |
| :- | :- | :- |
| `accepted` | `email.sent` | Message accepted by Mailofly |
| `delivered` | `email.delivered` | Accepted by recipient ISP |
| `failed` (severity: permanent) | `email.bounced` | Hard bounce |
| `failed` (severity: temporary) | `email.delivery_delayed` | Soft bounce or temporary throttle |
| `complained` | `email.complained` | Recipient marked as spam |
| `opened` | `email.opened` | Tracking pixel loaded |
| `clicked` | `email.clicked` | Recipient clicked link |
| `unsubscribed` | `contact.unsubscribed` | Recipient opted out |

***

## Migration Checklist

* [ ] Add domain to Mailofly and configure DKIM (`mailofly._domainkey`) and SPF.
* [ ] Add `include:mailofly.com` to your domain SPF record.
* [ ] Export Mailgun Bounces, Complaints, and Unsubscribes, and import into Mailofly.
* [ ] Update client application code from `multipart/form-data` to `@mailofly/node` JSON calls.
* [ ] Configure Mailofly webhook URLs for delivery and bounce tracking.
* [ ] Verify test emails on the Mailofly dashboard.
* [ ] Remove Mailgun MX and SPF records after the transition is complete.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.