Migrate from Postmark to JustEmails API: 2026 Cutover Guide
Postmark to JustEmails migration without losing emails.
Postmark to JustEmails migration without losing emails.
Picture the month-end invoices: transactional email on Postmark, mailbox hosting somewhere else, a separate Resend account for the side project. Three providers, three dashboards, three sets of credentials. It's a mess.
Look, Postmark is genuinely good. Excellent deliverability. Clean dashboard. Their bounce handling? Widely respected. This isn't a hit piece.
But when you're already using JustEmails for mailbox hosting across multiple domains, and you realize the $49/year plan includes 1,000 transactional emails/month with a straightforward API, the consolidation math starts to look attractive. One provider for mailboxes and transactional. One DNS configuration per domain. One invoice. (Setup takes longer than you'd guess, mostly because of DNS propagation and template conversion. More on that later.)
This is a cutover guide for moving a live service. The goal: zero dropped emails, minimal downtime risk, and a clean break from the old provider.
A fully working transactional email setup on the JustEmails API — password resets, order confirmations, notifications, whatever you're sending — with your existing domain verified, webhooks receiving delivery events, and Postmark ready to decommission. Hands-on time: 2-3 hours spread across a week. Maybe four if your templates are a tangle.
Before you start:
One thing people skip: check your current Postmark sending volume. Log into Postmark, go to Activity, and note your daily/monthly numbers. You'll need this to pick the right JustEmails API tier. The $49/year plan includes 1,000/month; add $25/year for each additional 10K/month tier. Skip this step and you may find out mid-migration that you need a bigger tier.
Note the daily figure especially, because the daily ceiling binds before the monthly allowance does: a newly verified domain sends no more than 50 a day in the first week, 200 in the second, and up to 500 from day 15, with mailbox sends and API sends counted against the same per-account number. A tier raises the monthly allowance rather than that daily cap, which puts the practical maximum at about 15,000 a month. Warm-up is measured from domain verification rather than signup, so adding this sending domain to a long-standing account starts that domain back at 50 a day.
API access requires a paid plan. This cutover runs on the transactional API, and API access is not included in the 7-day trial — a trial account cannot issue the key Step 2 asks for, so there is no way to complete the migration without subscribing first.
Don't start changing code until you know exactly what you're migrating.
In Postmark, document:
Message Streams: Postmark separates transactional and broadcast streams. List each stream and what it sends. JustEmails doesn't enforce this separation at the API level, but you'll want to categorize for your own sanity.
Templates: Export every template. Postmark's API has a GET /templates endpoint (it requires count and offset), or you can manually copy from the dashboard. The list gives you names and IDs; fetch each template by ID for its HTML, text version and variable names.
curl "https://api.postmarkapp.com/templates?count=100&offset=0" \
-X GET \
-H "Accept: application/json" \
-H "X-Postmark-Server-Token: your-token" > postmark-templates.json
Webhook URLs: Note every webhook endpoint you've configured — deliveries, bounces, spam complaints, opens, clicks. You'll replicate these for JustEmails.
Sending volume: Average daily sends, peak sends, monthly total. This determines your tier.
Twenty minutes here saves you from discovering a forgotten webhook halfway through the cutover.
If you're already using JustEmails for mailboxes on this domain, the domain is verified already; what you still need is an API key and a from address.
In JustEmails dashboard:
noreply@yourdomain.com) exists as a mailbox or alias on the domain — the send endpoint refuses any other from addressIf this is a new domain to JustEmails, add it now and complete the verification flow. You'll add a TXT record for domain ownership.
DNS changes — the tricky part:
JustEmails signs from dkim._domainkey, a TXT record whose key is unique to your domain. Postmark uses its own DKIM selector (usually 20xxxxxx._domainkey). Different selectors, so during migration you can publish both.
Add the JustEmails DKIM record alongside Postmark's, copying the value from your dashboard rather than from this page:
dkim._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=…"
Update your SPF record to authorise both (temporarily). Postmark uses an include:; we use an a: mechanism pointed at mail1.justemails.app, so the two sit side by side:
v=spf1 include:spf.mtasv.net a:mail1.justemails.app ~all
Don't remove Postmark's records yet. You're running both in parallel. Removing them before you've fully migrated will break DKIM alignment on any emails still going through Postmark. For a deep dive on email authentication, see our SPF, DKIM, and DMARC setup guide.
Here's where Postmark and JustEmails differ philosophically.
Postmark stores templates on their servers. You send a template ID and variables; they render it. JustEmails expects you to render templates in your application and send the final HTML.
The migration approach:
Example — before (Postmark):
// Postmark's approach
await postmark.sendEmailWithTemplate({
TemplateId: 12345,
TemplateModel: {
name: 'Sarah',
resetLink: 'https://app.com/reset?token=abc'
},
To: 'sarah@example.com'
});
After (JustEmails with local template):
// Local template rendering
import Handlebars from 'handlebars';
import { passwordResetTemplate } from './templates/password-reset.js';
const template = Handlebars.compile(passwordResetTemplate);
const html = template({
name: 'Sarah',
resetLink: 'https://app.com/reset?token=abc'
});
await fetch('https://justemails.app/api/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.JUSTEMAILS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'YourApp [noreply@yourdomain.com](mailto:noreply@yourdomain.com)',
to: ['sarah@example.com'],
subject: 'Reset your password',
html: html,
text: 'Click here to reset: ...' // plain text fallback
})
});
Is this more work? Yes. Honestly, it can feel like a step backward from Postmark's convenience. But your templates then live in version control, you can preview them locally, and you're not locked into a provider's template syntax. I think that trade is worth it. Your mileage may vary.
Here's the core migration technique: send through JustEmails first, fall back to Postmark if it fails. Run this for 72 hours minimum. If JustEmails handles everything, you're good to cut Postmark.
// email-sender.js — dual-send wrapper
async function sendTransactional({ to, subject, html, text, idempotencyKey }) {
// Try JustEmails first
try {
const response = await fetch('https://justemails.app/api/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.JUSTEMAILS_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey // one key per message, reused on retries
},
body: JSON.stringify({
from: 'YourApp [noreply@yourdomain.com](mailto:noreply@yourdomain.com)',
to: [to],
subject,
html,
text
})
});
if (response.ok) {
console.log('Sent via JustEmails');
return { provider: 'justemails', success: true };
}
throw new Error(`JustEmails failed: ${response.status}`);
} catch (error) {
console.warn('JustEmails failed, falling back to Postmark:', error.message);
}
// Fallback to Postmark
try {
const response = await fetch('https://api.postmarkapp.com/email', {
method: 'POST',
headers: {
'X-Postmark-Server-Token': process.env.POSTMARK_TOKEN,
'Content-Type': 'application/json'
},
body: JSON.stringify({
From: 'noreply@yourdomain.com',
To: to,
Subject: subject,
HtmlBody: html,
TextBody: text
})
});
if (response.ok) {
console.log('Sent via Postmark (fallback)');
return { provider: 'postmark', success: true };
}
throw new Error(`Postmark also failed: ${response.status}`);
} catch (error) {
console.error('Both providers failed:', error.message);
throw error;
}
}
The idempotency key makes a retry safe: if a request times out but actually went through, re-sending it with the same key won't send a second copy. JustEmails supports idempotency keys on the send endpoint — use them. Generate one key per message and reuse it only when retrying that message. Don't derive it from the address and subject: the API answers 409 when a key is reused with a different body, so a user's second password reset would fail over to Postmark.
Postmark sends webhooks to your endpoints for bounces, spam complaints, deliveries, etc. JustEmails does too, but the payload format differs.
Create a new webhook endpoint or modify your existing one to handle both formats during transition:
// /api/email-webhooks.js
export async function POST(request) {
const payload = await request.json();
// Detect provider by payload shape
if (payload.RecordType) {
// Postmark format
return handlePostmarkWebhook(payload);
} else if (payload.event) {
// JustEmails format
return handleJustEmailsWebhook(payload);
}
return new Response('Unknown format', { status: 400 });
}
function handleJustEmailsWebhook(payload) {
// JustEmails posts { event, timestamp, data }
switch (payload.event) {
case 'email.delivered':
// record delivery
break;
case 'email.bounced':
// mark address as undeliverable
break;
}
}
Register your webhook URL in the JustEmails dashboard under Settings → Webhooks, and check the API docs for the full list of event names. Start receiving both providers' events simultaneously.
Deploy the dual-send wrapper to production. Then watch.
Check daily:
If JustEmails handles 100% of sends for 72 hours with no fallbacks, you're ready to cut Postmark.
If you see fallbacks, investigate. Common causes:
error.code names which one; while the domain is warming up the daily ceiling is 50, then 200, and no tier lifts it)Don't rush this step. Three days of parallel sending is cheap insurance.
(Resist turning Postmark off after 24 hours because "everything looks fine." A misconfigured webhook can hide for a day and surface as bounces you never saw.)
Once you're confident:
Remove Postmark fallback from code. Delete the fallback block from your email sender. JustEmails is now the only path.
Update DNS, 48 hours later. Once mail already queued at Postmark has had time to arrive, remove Postmark's DKIM records and its SPF include, leaving only our a: mechanism. Your SPF becomes:
v=spf1 a:mail1.justemails.app ~all
(Plus any other legitimate senders — marketing tools, etc.)
Disable Postmark webhooks. Either delete them or point them to a sink endpoint.
Keep Postmark account active for 30 days. In case you missed something. You can cancel after a month of clean operation.
Update your runbooks. If you have incident response docs that mention "check Postmark dashboard," update them.
401 Unauthorized from JustEmails API
The API key isn't reaching the header intact. JustEmails expects Authorization: Bearer <your key>. If you copied from an environment variable, check for trailing newlines or spaces.
It's always whitespace. Always.
429 Too Many Requests
Either the monthly allowance (base tier is 1,000/month; add tiers at $25/year per 10K/month), the daily send cap, which a tier does not lift, or an hourly limit. The response's error.code names which one. Wait for the Retry-After header rather than hammering the endpoint.
DKIM failures after cutover
You removed Postmark's DKIM records before all Postmark-sent emails were delivered. DKIM is checked on receipt, not send. If an email was queued at Postmark and delivered after you removed records, the signature fails. This is why you keep the records for 48 hours after cutover.
Templates rendering with wrong variables
Postmark's Mustachio syntax isn't 100% compatible with Handlebars/Liquid. Watch for edge cases like {{#if}} vs {{#unless}} and array iteration syntax. Test every template manually. Yes, all of them. It's tedious.
For a service sending 10,000 transactional emails per month:
The real savings isn't dollars — it's one fewer vendor to manage. One login, one DNS configuration, one support contact. Once everything sits on one provider, there's no more guessing which one handles what.
Fewer invoices. Fewer dashboards. Fewer passwords to rotate. It adds up.
Is Postmark's deliverability slightly better? Historically, yes — they've built reputation over a decade. But for most transactional email (password resets, receipts, notifications), the difference is negligible. You're not sending cold outreach through this channel.
For the rest of us consolidating SaaS tools, the JustEmails API handles the job.
Now that you've migrated:
The JustEmails API docs cover attachments, open and click tracking, and the full webhook event list. Contact support@justemails.app if you hit edge cases during migration.
And if you're not on JustEmails yet but considering it alongside Postmark — see our SMTP vs API comparison for when HTTP API makes sense versus relay. Spoiler: if you're already on Postmark's API, you're the target audience.
Not because Postmark is bad — Postmark's deliverability is excellent. You'd migrate to consolidate mailbox hosting and transactional email under one provider. If you're already paying $49/year for JustEmails mailboxes, you get 1,000 transactional emails/month included. Adding tiers is $25/year per 10K emails/month. For teams inside the 500-a-day send ceiling — roughly 15,000 a month, with mailbox and API sends counted together — consolidation often makes sense.
Not if you use the dual-send window approach. Keep both providers active for 72 hours minimum. Send through JustEmails first (so it builds reputation on your domain), then fall back to Postmark if JustEmails fails. Once you've confirmed zero failures over 72 hours, remove the Postmark fallback.
JustEmails doesn't have server-side template storage like Postmark. You'll render templates in your application code — Handlebars, Liquid, Jinja, whatever your stack uses. Export your Postmark templates via their API, convert them to your local template engine, and pass the rendered HTML to the JustEmails send endpoint. It's more work upfront but gives you version control over templates.
Only if the domain isn't on JustEmails yet. Either way, each provider has its own DKIM keys. You'll add JustEmails' DKIM records alongside Postmark's during the transition, then remove Postmark's after cutover. SPF needs updating too — add our a:mail1.justemails.app mechanism (we don't publish an SPF include), keep Postmark's include until you're fully migrated, then remove it. DMARC stays the same; just watch alignment reports.
JustEmails is $49 a year, flat, for unlimited custom domains and unlimited mailboxes with a unified inbox across all of them — additional team members are $2 per member per month, and storage beyond the included 10 GB is $99 a year per 100 GB. Built for agencies, freelancers, and anyone managing email across more than one domain.