Compare providers

Postmark vs Mailgun

Postmark and Mailgun both come from the developer-tools generation of email providers, but they optimize for different things. Postmark optimizes for deliverability and clarity: separate transactional and broadcast message streams, aggressive list hygiene, detailed delivery events, and a UI your support team can actually use. Mailgun optimizes for flexibility: powerful routing and inbound parsing, EU region hosting for data residency, send-time optimization, and native scheduled delivery — the one field in the unified message shape Postmark doesn't support. Both carry CC, BCC, reply-to, headers, attachments, tags, and metadata, so the SDK can fail over between them for most messages. Teams typically choose Postmark for critical low-volume transactional mail and Mailgun when inbound processing or EU residency is a requirement — or run both, with one as the fallback route.

Message field support

Field support as encoded in Email SDK's own adapter capability matrix — the same data the SDK uses to reject sends that would silently drop fields.

Message fieldPostmarkMailgun
CC recipients
BCC recipients
Reply-To address
Custom headers
Attachments
Tags
Metadata
Scheduled sending (sendAt)

Fallback compatibility

Failing over from Mailgun to Postmark loses: scheduled sending (sendat).

Same code, either provider

With Email SDK the send call is identical for both providers — only the adapter import changes:

import { createEmailClient } from "@opencoredev/email-sdk";
import { postmark } from "@opencoredev/email-sdk/postmark";

const client = createEmailClient({
  adapters: [postmark({ serverToken: process.env.POSTMARK_SERVER_TOKEN! })],
});

await client.send({
  from: "hello@yourdomain.com",
  to: "user@example.com",
  subject: "Welcome!",
  html: "<p>It works.</p>",
});

Or run both, with automatic fallback:

import { createEmailClient } from "@opencoredev/email-sdk";
import { postmark } from "@opencoredev/email-sdk/postmark";
import { mailgun } from "@opencoredev/email-sdk/mailgun";

const client = createEmailClient({
  adapters: [
    postmark({ serverToken: process.env.POSTMARK_SERVER_TOKEN! }),
    mailgun({ apiKey: process.env.MAILGUN_API_KEY!, domain: "mg.yourdomain.com" }),
  ],
  defaultAdapter: "postmark",
  fallback: ["mailgun"],
});

FAQ

Can I switch from Postmark to Mailgun without rewriting my email code?
Yes. With Email SDK both providers share one typed send() call and one message shape, so switching from Postmark to Mailgun is a one-line adapter change plus an API key. The SDK fails fast if a message uses a field Mailgun does not support, so nothing is silently dropped.
Which message fields does Postmark support that Mailgun doesn't?
Mailgun supports every message field that Postmark supports, so nothing is lost moving a message from Postmark to Mailgun.
Can I use Mailgun as a fallback for Postmark?
Partially. Email SDK checks field support before every send: a fallback from Postmark to Mailgun is rejected for messages using no fields, and from Mailgun to Postmark for messages using scheduled sending (sendat). Messages that avoid those fields fail over cleanly.

Adapter docs

Setup guides: Postmark adapter and Mailgun adapter. Sending from your own domain? Verify SPF, DKIM, and DMARC with the free email DNS checker.