Mailgun
Send through the Mailgun Messages API with domain-scoped multipart sends, h:/v:/o: field mapping, and full field support.
Capabilities
| Repeated headers | Idempotency | Scheduling | Personalized |
|---|---|---|---|
| Yes | none | Yes | native |
These values come from the adapter's exported capabilities declaration. The field support matrix covers normalized message fields.
The Mailgun adapter calls the Mailgun Messages API with plain fetch. It is the one adapter that posts multipart form data instead of JSON, using Mailgun's prefixed field names: h: for headers, v: for metadata variables, o: for sending options. Every send is scoped to the domain you configure.
Configure
Grab an API key from the Mailgun dashboard and note the sending domain it belongs to. Both are required.
import { createEmailClient } from "@opencoredev/email-sdk";
import { mailgun } from "@opencoredev/email-sdk/mailgun";
export const email = createEmailClient({
adapters: [
mailgun({
apiKey: process.env.MAILGUN_API_KEY!,
domain: process.env.MAILGUN_DOMAIN!,
}),
],
});Prop
Type
EU domains need the EU base URL
Domains created in Mailgun's EU region only respond on baseUrl: "https://api.eu.mailgun.net".
Against the default US origin they fail with a 404.
Send
Mailgun maps every common field: cc, bcc, replyTo, headers (as h:Name), attachments (multipart attachment/inline parts by disposition), tags (values as o:tag), and metadata (as v:key variables, surfaced later in webhooks and logs). sendAt becomes a native scheduled send via o:deliverytime (RFC 2822); Mailgun accepts up to 3 days out on most plans, 7 with extended storage.
const result = await email.send({
from: "Acme <hello@mg.acme.com>",
to: "user@example.com",
replyTo: "support@acme.com",
subject: "Your receipt from Acme",
html: "<p>Thanks for your order.</p>",
metadata: { orderId: "ord_123" },
tags: [{ name: "type", value: "receipt" }],
});
console.log(result.id); // Mailgun message idThe response id is Mailgun's queued message id from the response body. Match it against delivery events in webhooks or the Mailgun logs.
Personalized sends
Mailgun implements native sendPersonalized, so all recipients and variables travel in one request through Mailgun's recipient-variables field.
await email.sendPersonalized({
message: {
from: "Acme <hello@mg.acme.com>",
subject: "Hi %recipient.name%",
html: '<a href="https://acme.com/unsub?id=%recipient.id%">Unsubscribe</a>',
},
recipients: [
{ to: "ada@example.com", variables: { name: "Ada", id: "u_1" } },
{ to: "linus@example.com", variables: { name: "Linus", id: "u_2" } },
],
});Verify from the CLI
MAILGUN_API_KEY="key-..." MAILGUN_DOMAIN="mg.acme.com" \
npx email-sdk doctor --adapter mailgunMAILGUN_API_KEY="key-..." npx email-sdk send \
--adapter mailgun \
--domain mg.acme.com \
--from "Acme <hello@mg.acme.com>" \
--to user@example.com \
--subject "Mailgun smoke test" \
--text "It works" \
--dry-runCredentials also work as flags: --api-key, --domain, and --base-url (or MAILGUN_BASE_URL) override the environment. Drop --dry-run for one real send to prove the domain's DNS records and the account are actually ready.
Compare Mailgun with other providers
Side-by-side message-field support from the SDK capability matrix, with working code for both adapters:
