ESP: Mailgun7 min read

Mailgun is API-first — seed every send in three lines.

Mailgun does not have a campaign UI to remember. Every send is an API call, which means every send can be seeded by wrapping one helper. Do it once, inherit placement data forever.

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.

TL;DR

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.

Get 20+ seed addresses free

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.

→ Run a free test now

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:

  1. 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.
  2. 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.
  3. 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 to instead of bcc. Putting seeds in to means every real user sees the seed list in their header. Use bcc.
  • 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.

Frequently asked questions

Will bcc'ing seeds slow down my Mailgun API calls?

No. Mailgun processes bcc at the same latency as a single-recipient send. You pay for one API call, and Mailgun fans out internally. The round-trip from your app is identical.

Does Mailgun count bcc recipients against my monthly send quota?

Yes. Each bcc counts as one recipient. With 20 seeds on 10,000 transactional sends a month that is 200,000 extra recipients. Use the sampling policy for high-volume templates to keep the cost sensible.

Can I seed templates stored in Mailgun's template store?

Yes. The template engine and the bcc are independent. Pass template: 'reset' plus bcc: SEED_ADDRESSES in the same /messages call. Mailgun renders the template for every recipient, real or seed.

How do I know seed reputations are not polluting my real domain reputation?

They do contribute, but positively. Seed providers mark seeded mail as read or leave it in inbox (depending on the seed-tool contract). They rarely complain. The net effect on your sender reputation is neutral-to-positive, never harmful.
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required