$ migrate
Moving from Mailgun to Pharos
Two of Mailgun’s four records do something other than authenticate mail. The MX records receive it, and the CNAME rewrites the links inside messages you have already sent. Deleting either on cutover day breaks something that has nothing to do with whether your new provider works.
The dangerous record in a Mailgun migration is the MX. Suppressions are three lists per sending domain, the tracking CNAME breaks click links if it is removed before the links expire, and inbound routes have no equivalent on Pharos.
The order the work happens in.
-
01
Export the suppression list
Pull every suppressed address out of Mailgun before anything else changes.
-
02
Verify the domain on Pharos
Add four DNS records alongside the ones already there. Both providers stay authenticated.
-
03
Move the send call
Point the application at POST /v1/send or the SMTP relay, behind whatever flag you use.
-
04
Ramp the traffic
Move a share of sending across, watch the events, increase it. No cutover window is required.
-
05
Repoint the webhook and clean up DNS
Move the event consumer, then remove the Mailgun records once nothing depends on them.
Steps two and three are reversible and can overlap for as long as you like. An SPF record may name both providers, DKIM selectors are independent of one another, and the single DMARC record does not change at any point. That is what makes a phased migration possible: there is no moment where the domain is authenticated for the new provider and not the old one.
The MX records are not part of sending, and removing them is not part of this migration.
Mailgun asks for MX records on the sending domain so it can receive mail and run Routes. If you have ever set up a route — forwarding support@ to a helpdesk, posting inbound mail to your application, storing replies — those MX records are load bearing for something Pharos does not do at all. Decide where inbound goes before you touch DNS. If the answer is "nowhere, we never used it", the records can go at the end with everything else.
Step 1 — export the suppression list.
Three lists — bounces, unsubscribes and complaints — and they are per sending domain rather than per account. An account sending from three domains has nine exports. There is also an allowlist, which is worth reading before you import anything: an address on it was deliberately exempted from suppression, and importing it as suppressed silently reverses a decision someone made on purpose.
# three lists, per domain, paginated
for domain in mg.yourdomain.com mg.otherdomain.com; do
for list in bounces unsubscribes complaints; do
curl -s -u "api:$MAILGUN_API_KEY" \
"https://api.mailgun.net/v3/$domain/$list?limit=1000" \
| jq -r --arg l "$list" '.items[] | [.address, $l] | @csv' \
>> suppressions.csv
done
done
# check the allowlist before importing anything
curl -s -u "api:$MAILGUN_API_KEY" \
"https://api.mailgun.net/v3/mg.yourdomain.com/whitelists?limit=1000" | jq -r '.items[].value' Paginate past the first page with the paging.next URL each response carries. A domain with more than a thousand bounces is normal, and a truncated export is the kind of error that only shows up as a complaint rate three weeks later.
Pharos suppresses an address automatically on a hard bounce and on a complaint, keeps the list per account, and lets you export it again whenever you like — including on the way out, if this page ever needs writing in the other direction. The difference between a hard and a soft bounce decides which addresses end up on it.
Step 2 — what is in DNS now, and what gets added.
Four record types, and only the first two are about sending. A migration touches the SPF include and the DKIM record; the other two need a decision rather than an edit.
The Mailgun records
| Type | Host | Value | What it does |
|---|---|---|---|
| TXT | mg.yourdomain.com | v=spf1 include:mailgun.org ~all | Their SPF. If you send from a subdomain this is its own record; if you send from the apex it is merged into yours. |
| TXT | smtp._domainkey.mg.yourdomain.com | k=rsa; p=MIGfMA0GCSqGSIb3… | DKIM, on a selector from their control panel. Does not collide with a Pharos selector. |
| CNAME | email.mg.yourdomain.com | mailgun.org | Click and open tracking. Every tracked link in every message you have already sent resolves through this hostname. |
| MX | mg.yourdomain.com | 10 mxa.mailgun.org · 10 mxb.mailgun.org | Receiving and Routes. Not a sending record, and not something Pharos replaces. |
Retire the tracking CNAME last, and not on the same day as anything else. A password-reset mail sent an hour before you deleted it still has a link pointing at that hostname, and a recipient clicking it gets a DNS failure rather than your site. Leave it resolving until the oldest message that matters has expired.
The four Pharos records
Added alongside, never instead of. Pharos verifies all four before the first send and re-checks them daily, so a record quietly edited six months from now surfaces as a warning rather than as a delivery failure.
| Type | Host | Value | Note |
|---|---|---|---|
| TXT | yourdomain.com | v=spf1 include:pharosinfra.net ~all | Merged into the SPF record you already have — one TXT record per domain, never two. |
| TXT | ph1._domainkey.yourdomain.com | v=DKIM1; k=rsa; p=MIIBIj… | The ph1 selector. It does not collide with the selector your current provider signs with. |
| CNAME | bounce.yourdomain.com | bounce.pharosinfra.net | The aligned return path, so the envelope sender is on your domain rather than the gateway’s. |
| TXT | _dmarc.yourdomain.com | v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com | One record per domain. If you already have one, it does not change during a migration. |
One thing to check before you add the include: SPF permits ten DNS lookups per evaluation, and two providers named in one record is two includes' worth of them. If you are already close, adding the second one is what breaks it — and it breaks silently. The ten-lookup limit explains how to count yours.
Steps 3 and 4 — move the call, then the traffic.
The send call is plain HTTP. There is no SDK to install and nothing to import, which means the change in your application is a URL, a header and a body shape rather than a dependency.
curl -X POST https://api.pharos.email/v1/send \
-H "Authorization: Bearer $PHAROS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4471" \
-d '{
"from": "receipts@yourdomain.com",
"to": "customer@example.com",
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>"
}' If you would rather not touch application code at all, point your existing mailer at the SMTP relay on smtp.pharosinfra.net:587 with STARTTLS. Same pipeline, same events, and a migration that is four lines of configuration. The Idempotency-Key header matters more during a migration than at any other time: a retry across a half-switched deployment is exactly the situation that sends a password reset twice.
Then ramp. Send a percentage, read the events, raise it. There is no warmup requirement to satisfy here — the sending IPs are dedicated to Pharos and already have a reputation — but ramping is still how you find the template that renders wrong or the address list nobody had cleaned, at a volume where finding it is cheap.
Step 5 — the event webhook.
Mailgun’s webhook event types map more cleanly than the other two, because both describe the same SMTP reality. The difference worth handling is the failure split: Mailgun tells you permanent or temporary in the event name, and Pharos tells you by only emitting an event once the outcome is final.
| Mailgun | Pharos | Note |
|---|---|---|
| accepted | accepted | The message was taken for delivery. |
| delivered | delivered | The receiving server accepted it. |
| failed (permanent) | failed | Permanent failure. Adds the address to suppression. |
| failed (temporary) | — | Retried internally. Pharos emits delivered or failed once it is settled. |
| rejected | rejected-by-system | Refused before it reached the network. |
| complained | complained | Counts against the 0.1% complaint rate in the AUP. |
| unsubscribed | unsubscribed | The recipient opted out. |
| opened | opened | Only when open tracking is on. |
| clicked | clicked | Only when link tracking is on. |
Mailgun signs webhooks with a timestamp, token and HMAC triple; Pharos signs with a single header over the raw body. Both need the raw bytes, so if your current handler parses JSON before verifying, fix that during the move rather than after.
A dash means Pharos emits nothing equivalent. In every case on this page that is because the event describes something Pharos does not do, rather than something it does quietly — the full list is in the events reference, and what is worth alerting on is in what to monitor on a transactional sender.
When to stay on Mailgun.
Four reasons not to do any of the above. They are here because you would find them during the migration otherwise, and finding them then costs a great deal more than reading them now.
-
You use Routes for inbound mail
Forwarding, storing and posting inbound mail to your application is a Mailgun feature with no Pharos equivalent. This is the single most common reason a Mailgun migration should be partial rather than total.
-
You use mailing lists
Mailgun’s mailing lists send one message to a stored group of members. Pharos carries transactional mail only and has nothing that stores a list of recipients for you.
-
You use their email validation
Address validation as a service is a separate product line there and does not exist here. If it is wired into your signup flow, it needs its own answer before the sending migration matters.
-
You are on a dynamic IP pool at high volume
If your sending is large enough that pool assignment is something you actively manage, you are operating at a scale where the honest conversation happens before the migration rather than during it.
Pharos carries transactional mail only — mail a recipient's own action caused. That single constraint is the reason for most of the gaps above, and it is written down in the acceptable use policy rather than discovered at suspension time.
Moving from somewhere else.
-
Moving from SendGrid to Pharos
Export all five SendGrid suppression lists, run both providers authenticated at once, move the send call, and map the event webhook. Step by step.
-
Moving from Postmark to Pharos
Dump suppressions per message stream, keep complaint suppressions permanent, move the transactional stream only, and map the webhooks. Step by step.
Nothing about this needs a maintenance window.
The free plan carries every feature and needs no card, which is enough to verify a domain and send the first test alongside whatever you run today.
start free