Guide
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 first for the overall order and how to run both providers in parallel.
Concepts
| Mailgun | Emailit |
|---|---|
| Account and subaccounts | Account and workspaces. Each workspace has its own domains, keys, members and credits. |
Domain, with its own API path /v3/<domain>/… |
Sending domain. There’s one send endpoint, and Emailit picks the domain from the from address. |
| Private API key | Full Access API key |
| 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 per workspace, with an alias and versions |
| Webhooks per domain | Webhooks per workspace |
| Routes | Inbound email with the email.received webhook, or the Forward received email automation |
| Suppressions per domain: bounces, unsubscribes, complaints | One suppression list per workspace |
| Mailing lists | Audiences |
| Tags and custom variables | meta |
| Logs and events | Email APIEmails, Email APIEvents and Email APILogs |
| Email validation | 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:
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>'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.
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.
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} |
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 indata.object. Usedata.object.id, theem_ID from the send response, to match events to messages. Yourmetavalues are indata.object.meta. - Mailgun signs a timestamp and token inside the body. Emailit signs the whole raw body: verify
X-Emailit-SignatureagainstX-Emailit-Timestampand yourwhsec_secret. See Request signature.
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
-
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,/complaintsand/unsubscribes). -
Build one CSV with the columns
email,type,reason:email,type,reason old-address@example.com,recipient,mailgun bounce angry@example.com,recipient,mailgun complaintUse the type
recipientfor addresses that must never receive email. It blocks API, SMTP and campaign sends. The typesbounce,complaintandunsubscribeonly stop campaigns. -
In Email APISuppressions, 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.
Move templates
Copy each template’s HTML from Mailgun, then import it in Email MarketingTemplates or create it with the Templates API. 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 and Import and export templates.
Change DNS
Add each domain in Email APIDomains 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.
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, which receives on a subdomain such as inbound.acme.com.
Next steps
- Go-live checklist
- Set up webhooks
- Priority migration: let Emailit engineers do the move with you