$ migrate
Moving from Postmark to Pharos
This is the easiest of the three moves and the one most likely to be a mistake, because Postmark is good at the thing Pharos is trying to be good at. The section on when to stay is at the bottom and is not decoration. If you are moving anyway, the mechanics are below.
Postmark keeps suppressions per message stream and per server, so a migration means one dump per stream, not one per account. The DNS work is additive, the transactional stream is the only one that belongs on Pharos, and spam-complaint suppressions must stay suppressed.
The order the work happens in.
-
01
Export the suppression list
Pull every suppressed address out of Postmark 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 Postmark 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.
Suppressions are scoped per message stream, per server — not per account.
The dump endpoint takes a stream id and a server token. An account with a transactional server and a separate broadcast server, each with its own streams, has a suppression list for each, and a single dump returns one of them. Enumerate your servers, enumerate the streams on each, and dump every one. Then note which suppressions carry the reason SpamComplaint: Postmark will not let you delete those on their side, and you should not let yourself delete them on the new side either.
Step 1 — export the suppression list.
One call per stream, with that server’s token. The reason field is the part worth keeping: HardBounce, SpamComplaint and ManualSuppression mean different things on the way in, and flattening them to one list throws away the distinction that tells you which addresses may ever be reactivated.
# list the streams on this server
curl -s "https://api.postmarkapp.com/message-streams" \
-H "Accept: application/json" \
-H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
| jq -r '.MessageStreams[].ID'
# dump one stream's suppressions, keeping the reason
curl -s "https://api.postmarkapp.com/message-streams/outbound/suppressions/dump" \
-H "Accept: application/json" \
-H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
| jq -r '.Suppressions[] | [.EmailAddress, .SuppressionReason] | @csv' \
>> suppressions.csv
# repeat for every stream, on every server token you hold Import the whole file. Every address on it should arrive at Pharos suppressed, and the ones marked SpamComplaint should never be reactivated by anyone, on either side.
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.
Postmark authenticates with a DKIM TXT record on a selector they issue, and a Return-Path CNAME. There is no SPF include to merge, because mail leaves under their own return-path domain — which is also why removing their records later is a smaller job than on the other two providers.
The Postmark records
| Type | Host | Value | What it does |
|---|---|---|---|
| TXT | 20240101._domainkey.yourdomain.com | k=rsa; p=MIGfMA0GCSqGSIb3… | DKIM. The selector is a date string they generate, so it will not collide with a Pharos selector. |
| CNAME | pm-bounces.yourdomain.com | pm.mtasv.net | The return path. This is the record that makes their DMARC alignment work, and the one to delete last. |
| TXT | _dmarc.yourdomain.com | v=DMARC1; p=none; | Unchanged throughout. One record per domain regardless of how many providers sign your mail. |
Because there is no SPF include, adding the Pharos include does not change your lookup count against anything they put there. If your SPF record is already close to the ten-lookup limit, it is close for some other reason, and this is a good moment to find out which.
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.
Postmark splits its webhooks by type — one URL per event kind, each configured separately. Pharos emits every event to one signed endpoint and keeps 120 days of them on GET /v1/events. If you currently run five handlers on five paths, you are consolidating rather than translating.
| Postmark | Pharos | Note |
|---|---|---|
| Delivery | delivered | The receiving server accepted it. |
| Bounce (HardBounce) | failed | Permanent failure. Adds the address to suppression. |
| Bounce (SoftBounce / Transient) | — | Pharos retries internally and emits the outcome, not the attempt. |
| SpamComplaint | complained | Counts against the 0.1% complaint rate in the AUP. |
| Open | opened | Only when open tracking is on. |
| Click | clicked | Only when link tracking is on. |
| SubscriptionChange | unsubscribed | The recipient opted out. |
| Inbound | — | Pharos does not receive mail. See the section on when to stay. |
The payload shape differs as well as the names — Postmark sends PascalCase fields, Pharos sends snake_case — so this is a rewrite of the handler body, not a change of URL.
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 Postmark.
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 send broadcast mail on a broadcast stream
Message streams let one account carry transactional and bulk mail with separate reputations. Pharos has no equivalent because it carries transactional mail only. If you use both streams, only one of them has anywhere to go here.
-
You process inbound mail
Inbound servers, inbound domain forwarding and inbound rules have no counterpart on Pharos. Nothing about this migration should involve deleting an MX record that something else depends on.
-
Your templates live in their editor
Pharos has versioned templates, but there is no importer. Templates authored and maintained in Postmark’s editor are a manual re-entry job, and the time that takes belongs in the decision rather than in a surprise during the cutover week.
-
Nothing is wrong
Postmark has run transactional mail for well over a decade and refuses bulk mail on principle, which is the same principle Pharos is built on. If your mail arrives, your support questions get answered and the bill is not the problem, the honest advice is to stay. A migration costs a week and buys you nothing you do not already have.
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 Mailgun to Pharos
Export bounces, unsubscribes and complaints per domain, leave the MX records alone until routes are handled, and retire the tracking CNAME last.
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