Setting Up a Seed-List Inbox Placement Test
Build a seed panel that reports where transactional mail actually lands: panel composition, fixture data, scheduling, and alerting on change rather than absolute share.
Your delivery log records that a receiver accepted the message. It does not record which folder the receiver put it in, and those are very different facts. A seed panel closes that gap by sending the real production template to accounts you control and reading back where each copy landed.
Root Cause: Acceptance Is Not Placement
An SMTP 250 OK means the receiving server took responsibility for the message. Filing happens afterwards, inside the provider, using signals the SMTP conversation never exposed. A message can be accepted and immediately placed in spam, and nothing in your logs will differ from a message placed in the inbox.
This is why deliverability dashboards that report "99.7% delivered" are describing acceptance rather than reach. The number is real and it is not the number anyone actually wants. Bridging that gap requires a mailbox you can inspect, which is what a seed panel is.
The limitation is equally important: seed accounts have no genuine engagement history. Providers that weight per-recipient behaviour treat an account that never opens anything differently from a real subscriber. Absolute placement on a panel is therefore pessimistic, and its value lies in comparison over time rather than in the raw percentage.
Panel Composition
Keep the panel small, real and fixed. Five or six accounts covering the providers your recipients actually use is sufficient, and each additional account adds maintenance without adding signal.
| Account | Why it is included |
|---|---|
| Gmail, consumer | Largest consumer share; also exposes tab placement |
| Outlook.com | Different filtering model, IP-weighted |
| Yahoo Mail | Third major consumer provider, distinct rules |
| iCloud Mail | Apple's filtering, plus Mail Privacy Protection behaviour |
| Microsoft 365 tenant | Corporate filtering, which can override sender reputation |
| A secondary Gmail | Detects account-specific noise in the primary result |
The last row is the one teams skip and then wish they had. Two Gmail accounts disagreeing tells you the result is account-specific noise rather than a real filtering change — which is the difference between an incident and a false alarm.
Do not change the panel. Adding or removing an account changes the denominator and destroys comparability with every previous run, which is the property the whole exercise depends on.
The Exact Implementation
Send the identical rendered message to every seed, wait, then read each mailbox and record the folder.
// placement/run.mjs — one render, many seeds, folder read back per provider.
import { renderTemplate } from "../render.mjs";
import { send } from "../esp/index.mjs";
import { readFolder } from "./mailboxes.mjs"; // IMAP or provider API per account
const PANEL = [
{ provider: "gmail", address: "seed-gmail@example.net", reader: "gmail-api" },
{ provider: "gmail-2", address: "seed-gmail2@example.net", reader: "gmail-api" },
{ provider: "outlook", address: "seed-outlook@example.net", reader: "imap" },
{ provider: "yahoo", address: "seed-yahoo@example.net", reader: "imap" },
{ provider: "icloud", address: "seed-icloud@example.net", reader: "imap" },
{ provider: "m365", address: "seed-m365@example.net", reader: "graph" },
];
export async function runPanel(templateId, runId) {
// Fixture data, identical every run. Varying content makes results unreadable,
// because a placement change could then be content or filtering.
const { html, text, subject } = await renderTemplate(templateId, {
customer: { name: "Seed Panel", email: "seed@example.net" },
order: { id: "SEED-0001", total: "£42.00" },
});
// A run marker in a custom header lets the reader find this exact run without
// depending on subject uniqueness, which would itself change the content.
await Promise.all(PANEL.map(({ address }) =>
send({ to: address, subject, html, text, headers: { "X-Seed-Run": runId } })
));
// Receivers file asynchronously. Poll rather than reading immediately, and
// give a generous window — some providers take several minutes.
const results = [];
for (const seed of PANEL) {
const folder = await readFolder(seed, runId, { timeoutMs: 15 * 60_000 });
results.push({ ...seed, folder }); // "inbox" | "spam" | "promotions" | "missing"
}
return { templateId, runId, results, at: new Date().toISOString() };
}
Two choices matter. The run marker in a custom header identifies the message without changing anything a filter evaluates — using a unique subject line would work but alters the content between runs, which is exactly what must stay constant. And missing is a distinct outcome from spam: a message that appears in no folder was rejected or silently dropped, which is a different and more serious problem than being filed.
Alerting on Change, Not on a Threshold
The absolute inbox share on a panel is pessimistic and not directly meaningful. The signal is movement.
// Compare against the previous run for the same template. A provider that
// changed folder is worth an alert; a stable low share usually is not.
export function diffRuns(previous, current) {
const changes = [];
for (const now of current.results) {
const before = previous.results.find((r) => r.provider === now.provider);
if (!before || before.folder === now.folder) continue;
changes.push({
provider: now.provider,
from: before.folder,
to: now.folder,
// Moving out of the inbox is an incident; moving into it is good news.
severity: now.folder === "inbox" ? "info" : "warn",
});
}
return changes;
}
Require the change to persist across two consecutive runs before paging. Single-run flips are common on accounts with no engagement history, and paging on them trains people to ignore the alert.
Variant: A Hosted Panel
Commercial platforms provide the panel and the reading layer, which removes the mailbox plumbing at the cost of a subscription and of running on infrastructure that is not yours. The trade is usually worth it for breadth — a hosted panel covers many more providers than you would maintain — and worse for realism, since a heavily-used testing service's seed addresses can themselves acquire a reputation.
Where both are available, a small self-hosted panel for the providers that matter most and a hosted panel for breadth complement each other. The platform integration patterns are the same either way: submit, poll or receive a callback, and compare against the previous result.
Reading Gmail's Tabs as a Separate Outcome
Gmail files consumer mail into tabs — Primary, Promotions, Updates, Social — and a message in Promotions is in the inbox by every technical measure while being somewhere most recipients look at less often. For transactional mail this matters: a password reset in Promotions is a support ticket waiting to happen.
Record the tab as a distinct value rather than collapsing it into "inbox". The Gmail API exposes category labels directly, so the reader can return primary, promotions or updates instead of a single boolean, and a move from Primary to Promotions becomes visible as its own change.
What drives tab assignment is not fully documented, but the observable pattern for transactional senders is consistent. Messages carrying marketing signals — multiple calls to action, promotional language, a large image ratio, an unsubscribe link presented prominently — drift toward Promotions. Messages that are narrowly about an account action stay in Primary. This is one of the strongest practical arguments for keeping transactional templates free of marketing content: a receipt with a "you might also like" block is a different message to a classifier than a receipt.
The other lever is separation of streams. A subdomain that sends only account-action mail develops a per-sender pattern the classifier can learn; one that mixes a weekly digest with password resets gives it a mixed signal and both message types drift. That is the same argument the reputation model makes for separate subdomains, arriving from a different direction.
Pipeline Integration
Run the panel on a schedule rather than on every merge, because it takes minutes and produces a non-deterministic result. Daily against your highest-volume templates is a reasonable default, plus an on-demand run before a significant change to the sending identity — a new IP, a new signing domain, an ESP migration.
Route alerts to whoever owns deliverability and reputation rather than to the template author. Placement moves for sending-practice reasons far more often than for content reasons, and defaulting the alert to the last person who edited the template sends the investigation the wrong way.
Validation and Deployment Checklist
Frequently Asked Questions
Do seed results predict what real recipients see?
Not precisely. Seed accounts lack engagement history, so their placement is a floor rather than an expectation — real subscribers who open your mail generally do better. What the panel predicts reliably is change: if placement moves for the seeds, something moved for everyone.
Should the panel use production or test sending infrastructure?
Production. The point is to observe how your actual sending identity is treated, and a test IP or a different signing domain measures something else entirely. Send from the real path, with the real headers, at the real volume tier.
How often will the panel produce a false alarm?
With a fixed panel and a two-run persistence rule, rarely — perhaps once a month. Without the persistence rule, several times a week, which is why the rule matters more than the panel size for making the alert trustworthy.
Can we skip the panel if Postmaster Tools looks healthy?
They answer different questions. Postmaster Tools reports Gmail's aggregate view of your domain; the panel reports where a specific template landed at several providers. A healthy reputation with a template that lands in spam is entirely possible — usually from a content or structural problem the spam score check would also flag.
Related
- Spam Filter and Inbox Placement Testing — the deterministic half that runs in CI
- Reading a SpamAssassin Report Rule by Rule — diagnosing a content cause once placement drops
- Email Deliverability and Reputation — where placement alerts should be routed
- Litmus and Email on Acid Workflows — running the panel through a hosted platform
← Back to Spam Filter and Inbox Placement Testing