# Migrate from Mailgun

> Guide to move from Mailgun to Couryo. Mapping of the form-based send (from, to, o:tag, v:, h:, o:deliverytime, template and recipient-variables) to JSON, of the webhook events, of suppressions and receiving routes, and the DNS change.

Source: https://couryo.com/en/docs/migrate-mailgun

Mailgun takes the send as a form (`multipart/form-data`), with options under prefixes like `o:`, `h:` and `v:`. Couryo takes JSON, with one field for each thing. Follow the [safe migration order](https://couryo.com/en/docs/migration.md) and use this guide to translate each part.

## The send call

```bash title="Mailgun: POST https://api.mailgun.net/v3/example.com/messages"
curl -s --user "api:$MAILGUN_API_KEY" \
  https://api.mailgun.net/v3/example.com/messages \
  -F from="Shop <orders@example.com>" \
  -F to="ana@example.com" \
  -F subject="Order 1042 confirmed" \
  -F text="Thanks, Ana!" \
  -F html="<p>Thanks, Ana!</p>" \
  -F o:tag="order" \
  -F v:order_id="1042"
```

```bash title="Couryo: POST https://api.couryo.com/v1/emails"
curl https://api.couryo.com/v1/emails \
  -H "Authorization: Bearer $COURYO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -d '{
    "from": "Shop <orders@example.com>",
    "to": "ana@example.com",
    "subject": "Order 1042 confirmed",
    "text": "Thanks, Ana!",
    "html": "<p>Thanks, Ana!</p>",
    "tags": { "category": "order" },
    "metadata": { "order_id": "1042" }
  }'
```

At Mailgun, the domain goes in the URL and authentication is `api:` plus the key. At Couryo, the URL is always the same, the key goes in `Authorization: Bearer` and the domain comes from `from`. The answer is `202 { "id": "msg_...", "status": "queued" }`.

## Fields

| Mailgun | Couryo | Note |
|---|---|---|
| `from`, `to`, `cc`, `bcc`, `subject`, `text`, `html` | the same names | as JSON; `to` takes a string or a list, up to 50 recipients counting `cc` and `bcc` |
| `h:Reply-To` | `reply_to` | |
| `h:X-Anything` | `headers: { "X-Anything": "..." }` | |
| `o:tag` (many) | `tags` | key and value, e.g. `{ "category": "order" }` |
| `v:key` or `h:X-Mailgun-Variables` | `metadata` | back in every webhook (at Mailgun, `user-variables`) |
| `o:deliverytime` (RFC 2822) | `scheduled_at` (ISO 8601, up to 30 days) | [reschedule and cancel](https://couryo.com/en/docs/sending.md#reschedule-or-cancel) |
| `o:testmode` | a `ck_test_` key | captures without delivering, with simulated events |
| `o:tracking`, `o:tracking-opens`, `o:tracking-clicks` | [project tracking](https://couryo.com/en/docs/tracking.md) | on for marketing, off for transactional by default |
| `attachment`, `inline` (files) | `attachments[]` with `content` in Base64; inline images with `content_id` | |
| `template` + `t:variables` | `template` + `variables` | recreate the template in Couryo (see below) |
| `recipient-variables` (batch sending) | `POST /v1/emails/batch` | one item per recipient, up to 100 per call |
| `o:require-tls`, `o:dkim` | always | TLS and your domain's DKIM on every send |

## Templates

Mailgun templates use Handlebars. At Couryo, variables are `{{name}}` and `{{order.total}}`, with an optional fallback (`{{name|there}}`); blocks like `{{#if}}` and `{{#each}}` do not exist. Recreate each template under **Templates** (HTML, MJML or the visual editor) and send `template` with the name or the id and `variables`.

## Events and webhooks

| Mailgun | Couryo |
|---|---|
| `accepted` | `accepted` and `queued` |
| `delivered` | `delivered` |
| `failed` with `severity: temporary` | `deferred` |
| `failed` with `severity: permanent` | `bounced` (or `suppressed`, when the address was already suppressed) |
| `complained` | `complained` |
| `opened` | `opened` |
| `clicked` | `clicked` |
| `unsubscribed` | no event: the address goes to the marketing suppression list (`GET /v1/suppressions?reason=unsubscribe`) |

Mailgun signs with `timestamp`, `token` and `signature` inside the body. Couryo signs following [Standard Webhooks](https://couryo.com/en/docs/webhooks.md), in the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers (HMAC-SHA256), and retries for up to 3 days. A Couryo webhook takes the events you choose, for every domain of the project.

## Suppressions, lists and receiving

- Export the **Bounces** and **Complaints** lists from Mailgun and add them to Couryo with `stream: "all"`; **Unsubscribes** go with `stream: "marketing"`. The loop is in [copy the suppressions](https://couryo.com/en/docs/migration.md#copy-the-suppressions).
- Mailing lists: export the members as CSV and [import them as contacts](https://couryo.com/en/docs/contacts.md#import-from-csv), with the consent confirmation. Each list becomes a tag or a [topic](https://couryo.com/en/docs/contacts.md#topics-and-preferences).
- Receiving routes: at Couryo, [inbound](https://couryo.com/en/docs/inbound.md) (Pro plans and above) receives on a subdomain, like `reply.example.com`, and delivers each message to the `email.received` webhook.

## DNS

Mailgun asks for an `include:` in the domain SPF, a DKIM key with its own selector, the tracking CNAME (`email.`) and, to receive, MX records pointing to it. Couryo uses `couryo1._domainkey`, `couryo2._domainkey` and `bounces`, and asks for nothing in the root domain SPF, so both stay published during the migration.

1. Publish Couryo's records and wait for the domain to be verified.
2. Keep the DMARC record you already have; to get the reports in the [deliverability agent](https://couryo.com/en/docs/deliverability.md), add `mailto:reports@dmarc.couryo.com` to `rua`.
3. If you receive email through Mailgun (routes), **do not change the root domain MX** before the messages have somewhere else to go. Couryo inbound uses its own subdomain.
4. Once all traffic is on Couryo and 7 days have passed, delete the Mailgun DKIM key, the tracking CNAME and its SPF `include:` (if nothing else uses it).
