# Migrate from Mailgun (/migrate/mailgun)



Sendly implements Mailgun's `POST /v3/<domain>/messages`. Keep the official
`mailgun.js` SDK — change the **URL** and the **API key**.

* **Base URL:** `https://api.sendly.now/api/compat/mailgun` (the SDK appends `/v3/<domain>/messages`)
* **Auth:** HTTP Basic `api:<sk_...>` — Mailgun's basic-auth style, where the username is
  the literal `api` and the password is your key. Use a Sendly secret key as the key.

<Callout type="info">
  Mailgun does not publish an official Python SDK. Python senders can call the compat
  endpoint over plain HTTP (`multipart/form-data`) or use the [Sendly Python SDK](/sdks).
</Callout>

## The two-variable swap [#the-two-variable-swap]

```js title="Before (Mailgun)"
import formData from "form-data";
import Mailgun from "mailgun.js";

const mailgun = new Mailgun(formData);
const mg = mailgun.client({ username: "api", key: "your-mailgun-key" });

await mg.messages.create("yourdomain.com", {
  from: "you@yourdomain.com",
  to: "customer@example.com",
  subject: "Hello",
  html: "<p>Welcome aboard.</p>",
});
```

```js title="After (Sendly)"
import formData from "form-data";
import Mailgun from "mailgun.js";

const mailgun = new Mailgun(formData);
const mg = mailgun.client({
  username: "api",
  key: "sk_your_sendly_key",
  url: "https://api.sendly.now/api/compat/mailgun",
});

await mg.messages.create("yourdomain.com", {
  from: "you@yourdomain.com",
  to: "customer@example.com",
  subject: "Hello",
  html: "<p>Welcome aboard.</p>",
});
```

<Callout type="warn">
  The domain in the URL path (`yourdomain.com` above) **must match** the domain of your
  `from` address. A mismatch returns a `400` in Mailgun's error shape.
</Callout>

## Supported fields [#supported-fields]

| Field           | Support | Notes                                                                                |
| --------------- | ------- | ------------------------------------------------------------------------------------ |
| `from`          | Full    | Must be on a [verified domain](/guides/verifying-domains) that matches the URL path. |
| `to`            | Full    | Repeatable, as Mailgun sends it.                                                     |
| `cc` / `bcc`    | Full    |                                                                                      |
| `subject`       | Full    |                                                                                      |
| `html` / `text` | Full    | `html` is preferred when both are present. At least one is required.                 |
| `h:X-*` headers | Full    | The `h:` prefix is stripped.                                                         |
| `o:tag`         | Partial | Repeatable; mapped to Sendly tags (sanitized).                                       |
| `attachment`    | Full    | File parts are read and attached.                                                    |

Success comes back in Mailgun's shape:
`{ "id": "<id@domain>", "message": "Queued. Thank you." }`.

## Not supported [#not-supported]

Mailgun options beyond `o:tag` — scheduled delivery (`o:deliverytime`), tracking toggles,
and stored templates — are **not applied**; only the fields in the table above are
honored. Auth, validation, and domain-match failures return Mailgun's bare
`{ "message": ... }` error body with the matching status (a bad key is `401`), so
`mailgun.js`'s own error handling (`error.status` / `error.details`) fires as it does
today.
