Mailgun is the developer-first ESP. There is no WYSIWYG campaign builder, no drag-and-drop automation canvas — just a transactional HTTP API, a templates endpoint, and a suppression store. That simplicity is exactly why seeding Mailgun is cleaner than seeding Mailchimp or HubSpot: there is no UI to train a team on, only a send helper to edit.
Add the seed addresses as a comma-separated bcc on the Mailgun /messages call. Wrap it once inside yoursendMail() helper. Every transactional and marketing send from your app now generates a per-provider placement report.
Why Mailgun deserves a seed layer
Mailgun handles billions of messages a month for SaaS products that never see their own email. Password resets, invoice receipts, team invites, webhook digests — all go out through the API and none of them get a “preview” step the way a Mailchimp broadcast does. That invisibility is where deliverability problems hide. A reset email landing in Spam for a week is not a Mailgun bug; it is a reputation drift nobody is measuring.
Seeds fix the blind spot. Every important template (welcome, reset, invoice, re-engagement) is silently bcc'd to 20+ provider mailboxes whenever a real user triggers it. You do not need a new cron job or a separate QA flow — the tests run continuously on real traffic, and you see the placement change the moment reputation shifts.
The three-line change
If you are using the official mailgun.js SDK, the patch is adding one field to the message object. Here is the entire diff for the common Node.js helper.
// before
await mg.messages.create(DOMAIN, {
from: 'Acme <no-reply@acme.io>',
to: user.email,
subject: 'Reset your password',
html: renderTemplate('reset', { user, token }),
});
// after
await mg.messages.create(DOMAIN, {
from: 'Acme <no-reply@acme.io>',
to: user.email,
bcc: process.env.SEED_ADDRESSES, // <- the entire change
subject: 'Reset your password',
html: renderTemplate('reset', { user, token }),
});SEED_ADDRESSES is the comma-separated list of 20+ addresses you get from the free placement tool. Mailgun accepts a string or an array. That is the whole mechanical change — everything else is policy (which templates, how often, how to read results).
The raw curl equivalent
If you are testing from the terminal or building in a language without an SDK, the same call over curl:
curl -s --user "api:$MAILGUN_API_KEY" \
https://api.mailgun.net/v3/$MAILGUN_DOMAIN/messages \
-F from='Acme <no-reply@acme.io>' \
-F to='real-user@example.com' \
-F bcc='seed1@inboxcheck.io,seed2@inboxcheck.io,seed3@inboxcheck.io' \
-F subject='Reset your password' \
--form-string html='<p>Click <a href="...">here</a> to reset.</p>'Which templates to seed (and which to skip)
Seeding every single transactional send is overkill and will inflate Mailgun volume. The right policy is: seed the templates that matter, at a rate that lets you spot drift within an hour.
- Always-seed templates. Password reset, welcome, invoice, email verification, magic-link login. These are user-critical. Seeding 100% of these sends is fine; the extra Mailgun cost is trivial.
- Sample-seed templates. Digest, weekly newsletter, notification batches. Seed 1 in 50 — enough to catch drift without multiplying the send volume by 20.
- Never-seed. Per-user transactional bursts with PII in the body (a support-ticket webhook, a security alert containing an IP address). Either redact or exclude.
The policy lives in one place — your sendMail() helper — so you can flip a flag per template without touching 30 call sites.
function shouldSeed(template: string): boolean {
const ALWAYS = ['reset', 'welcome', 'invoice', 'verify', 'magic-link'];
const SAMPLE = ['digest', 'newsletter', 'notification-batch'];
if (ALWAYS.includes(template)) return true;
if (SAMPLE.includes(template)) return Math.random() < 0.02;
return false;
}
export async function sendMail(opts: SendOpts) {
const msg: Record<string, unknown> = {
from: opts.from,
to: opts.to,
subject: opts.subject,
html: opts.html,
};
if (shouldSeed(opts.template)) {
msg.bcc = process.env.SEED_ADDRESSES;
msg['v:seed-template'] = opts.template; // Mailgun custom variable
}
return mg.messages.create(DOMAIN, msg);
}Tagging seeded sends with Mailgun variables
Note the v:seed-template field. Mailgun custom variables (prefixed with v:) are returned on every webhook event. Tag every seeded send and you can filter Mailgun logs to only the seeded traffic — useful for correlating provider placement with Mailgun's own bounce and complaint counters.
The free Inbox Check tool generates 20+ fresh seed addresses per test across Gmail, Outlook, Yahoo, Mail.ru, Yandex, ProtonMail and more. No signup, no credit card.
Reading placement: what Mailgun will not tell you
Mailgun's dashboard tells you three things beautifully: delivered, bounced, complained. It tells you zero about folder. A message accepted by Gmail with a 250 OK is “delivered” in Mailgun's view even if Gmail put it in Spam. Seeds fill the gap:
- Gmail Promotions vs Primary. Mailgun cannot see this; seeds do. A steady drift from Primary to Promotions on a transactional template is a subject-line or template-markup problem, not a reputation one.
- Outlook Junk placement. Outlook accepts the SMTP hand-off and then silently files to Junk. Mailgun shows 250 OK; the user never sees the email. Only a seed in an Outlook.com mailbox catches it.
- Provider-specific filtering. Yahoo, Mail.ru and Yandex have independent classifiers. A template that lands Primary on Gmail can land Spam on Yandex on the same send. Your Mailgun metrics will look fine either way.
Common pitfalls when seeding Mailgun
- Using
toinstead ofbcc. Putting seeds intomeans every real user sees the seed list in their header. Usebcc. - Letting seeds hit the suppression store. Mailgun auto-suppresses bounced or complained addresses. If a seed provider briefly bounces, Mailgun will stop sending to that seed forever — and you will not notice. Periodically clear the Mailgun suppression list for seed domains or use the suppression-bypass flag.
- Mixing seeds across sending domains. Use a distinct seed set per sending domain if you have multiple Mailgun domains (transactional vs marketing). Reputations are per-domain and mixing them hides which one has a problem.
- Forgetting test mode. If Mailgun is in
test mode, messages are logged but never actually sent — seeds will not arrive. Only seed in production.