A burner email address is one of the simplest ways to make CI signup, invite, and verification tests less flaky. The mistake is treating it like a human inbox: logging in, scraping a web UI, reusing one shared mailbox, or hoping an email arrives before the next parallel job touches the same address.
For CI runs, a burner email address should be programmable. Your pipeline should create it on demand, pass it into the app under test, receive the resulting email as structured data, extract the verification link or code, and dispose of the inbox context when the run is done.
That pattern is especially useful when your tests are driven by automation frameworks, AI agents, or LLM based workflows. The goal is not just to receive an email. The goal is to make email a deterministic API dependency in your CI system.
What a CI burner email address needs to do
A consumer temporary inbox is usually designed for a person sitting in front of a browser. CI needs something different. It needs reliable isolation, machine readable output, and a retrieval method that works without a visual inbox.
At minimum, a CI friendly burner email address should support:
- Creating a fresh inbox through an API
- Returning the email address and a stable inbox identifier
- Receiving email content as structured JSON
- Supporting webhook delivery or polling
- Working with parallel jobs without mailbox collisions
- Providing enough metadata to match the right email to the right test
- Handling retries without reusing stale messages
Mailhook is built around this API first model: disposable inbox creation via API, structured JSON email output, RESTful access, real time webhooks, polling, shared domains, custom domain support, signed payloads, and batch email processing. You can also review the product capabilities in Mailhook’s llms.txt, which is useful when AI agents or LLM tools need a concise, machine readable description of the service.
The core pattern: one inbox per CI run or attempt
The safest default is to create a unique burner inbox for every CI run. If your test suite runs multiple signup or invitation tests in parallel, create an inbox per test case or per attempt instead.
Reusing one shared address creates hidden coupling. One job may read another job’s message. A retry may find an old verification email. A test may pass locally but fail when your CI provider increases parallelism.
The table below summarizes common isolation strategies.
| Strategy | Best for | Main benefit | Risk |
|---|---|---|---|
| One inbox per CI run | Small suites with serial email tests | Simple setup and teardown | Parallel tests can collide |
| One inbox per test file | Medium suites | Better separation with limited overhead | Collisions still possible inside a file |
| One inbox per test case | Parallel CI and signup flows | Strong isolation | More inboxes to track |
| One inbox per retry attempt | Flaky external systems or retry heavy pipelines | Avoids stale verification links | Requires careful attempt metadata |
If you are already seeing cross test contamination, stale links, or nondeterministic failures, a stronger inbox isolation model is usually the fix. For a deeper dive into that specific failure mode, Mailhook has a guide on how to create a disposable email address per test run without collisions.
Step 1: Create the burner email address during CI setup
The inbox should be created before the test that needs it, not manually in advance. In most CI systems, that means creating the inbox in a setup script, fixture, or test helper.
Avoid hardcoding a single email address into your CI secrets. Instead, your test helper should request a new disposable inbox and store the returned values in memory or in job scoped environment variables.
A provider neutral setup flow looks like this:
# Pseudocode only. Use the endpoint and auth format from your email API provider.
CREATE_INBOX_RESPONSE=$(curl -s \
-X POST "$MAIL_API_BASE/inboxes" \
-H "Authorization: Bearer $MAIL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label":"ci-signup-test"}')
export TEST_EMAIL=$(echo "$CREATE_INBOX_RESPONSE" | jq -r '.email')
export TEST_INBOX_ID=$(echo "$CREATE_INBOX_RESPONSE" | jq -r '.inbox_id')
Do not rely on the address alone if your provider returns an inbox ID. The address is what your application uses. The inbox ID is what your test automation should use to fetch or correlate messages.
For LLM driven agents, pass both values as explicit tool context rather than asking the model to remember them from the chat transcript. Treat the inbox ID as a workflow variable.
Step 2: Use the burner address in the app flow
Once the inbox exists, use the generated address exactly as a real user would. For example, your test might submit it to a signup form, invite a user to a workspace, or trigger a passwordless login code.
A simplified Playwright style example might look like this:
// Pseudocode. The inbox is created by your test fixture or CI setup.
const email = process.env.TEST_EMAIL;
await page.goto(process.env.APP_UNDER_TEST_URL);
await page.getByLabel('Email').fill(email);
await page.getByRole('button', { name: 'Create account' }).click();
The application should not need to know that this is a burner email address. From the app’s perspective, it is just an email destination. The test framework handles the inbox lifecycle outside the product flow.
Step 3: Receive the email as JSON, not as a page scrape
A CI pipeline should not log into an inbox UI. UI scraping adds timing problems, browser state, visual layout changes, and selectors that have nothing to do with the product you are testing.
Instead, fetch messages through an API or receive them through a webhook. Mailhook supports both real time webhook notifications and polling API access, which lets teams choose the retrieval method that fits their CI environment.
Webhook delivery is useful when your CI system or test harness can expose a receiver. Polling is often easier in hosted CI jobs where inbound webhooks are inconvenient. Either way, the important part is that the email arrives as structured JSON rather than as a rendered inbox page.
If your team is still comparing these approaches, the guide on how to see emails in CI without logging into a mailbox covers the tradeoffs between UI free options.
Step 4: Poll deterministically when webhooks are not practical
Polling can be reliable if you treat it as a bounded wait, not an infinite loop. Your test should wait for a specific message, match it against expected metadata, and fail with a useful error if it never arrives.
A basic polling helper should check:
- Inbox ID, not just recipient address
- Expected sender or sender domain
- Subject pattern or message type
- Timestamp after the test started
- Maximum timeout
Here is a simplified example:
async function waitForVerificationEmail({ inboxId, startedAt }) {
const timeoutMs = 60_000;
const intervalMs = 2_000;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const messages = await emailClient.listMessages({ inboxId });
const match = messages.find((message) => {
return message.created_at >= startedAt &&
message.subject.includes('Verify') &&
message.from.includes('your-app-domain.example');
});
if (match) return match;
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
throw new Error('Verification email did not arrive within 60 seconds');
}
The exact field names depend on your provider, but the principle stays the same: match the message you caused, within the time window you control.

Step 5: Extract the verification link or code safely
Once the message is available as JSON, extract only the value the test needs. That might be a one time password, a magic link, an invite URL, or a confirmation token.
For deterministic tests, prefer a normal parser over an LLM. If the message includes HTML, use an HTML parser and select links that match your expected domain and path. If it includes a numeric code, use a strict regular expression that matches the format your app sends.
For example:
function extractVerificationCode(textBody) {
const match = textBody.match(/\b\d{6}\b/);
if (!match) {
throw new Error('No six digit verification code found');
}
return match[0];
}
If an AI agent is coordinating the flow, use the LLM for orchestration rather than final assertions. Give the agent the JSON message as tool output, but keep link extraction, domain validation, and pass or fail criteria in code. That keeps your CI result reproducible.
Step 6: Follow the link or submit the code
After extraction, finish the user flow just as a user would. Visit the verification link in the browser context, or submit the code into the app.
For a link based flow:
const message = await waitForVerificationEmail({
inboxId: process.env.TEST_INBOX_ID,
startedAt: testStartedAt
});
const verificationUrl = extractVerificationUrl(message.html || message.text);
if (!verificationUrl.startsWith(process.env.APP_UNDER_TEST_URL)) {
throw new Error('Verification URL does not match the expected app domain');
}
await page.goto(verificationUrl);
await expect(page.getByText('Email verified')).toBeVisible();
For a code based flow, submit the code through the UI and assert the verified state. In both cases, keep the email handling separate from the product assertion. The email confirms delivery. The UI assertion confirms the application behavior.
Step 7: Make retries safe
CI retries can accidentally hide email bugs. A test fails, restarts, and then consumes the previous attempt’s email. The test passes, but your pipeline never verified that the new attempt produced a new message.
To avoid that, stamp every attempt with fresh context. Create a new burner email address per retry attempt, or at least filter emails by timestamp and unique metadata. When possible, include a test run identifier in the user name, organization name, or invite label submitted to the app. Then assert that the email content contains that same identifier.
For example, a run ID like ci-48291-attempt-2 can appear in the display name or workspace name. When the email arrives, your test can verify it belongs to the current attempt before extracting a link.
Step 8: Secure the email path
CI email testing often touches sensitive flows: account creation, passwordless login, invitations, and billing notifications. Treat the inbox path with the same care as any other test integration.
Use CI secrets for API tokens. Do not print full email payloads into public logs. Redact magic links, one time codes, and tokens before storing artifacts. If you use webhooks, verify signed payloads when your provider supports them. Mailhook includes signed payloads for security, which is especially helpful when inbound webhook events can trigger test actions.
For custom domains, use a domain or subdomain dedicated to automation. That keeps production or employee email infrastructure separate from test traffic. Mailhook supports custom domains as well as instant shared domains, so teams can start quickly and later move to a domain model that better matches their CI and compliance needs.
Common CI failure modes and fixes
Email based tests fail for predictable reasons. The fixes are usually architectural rather than cosmetic.
| Failure mode | Likely cause | Fix |
|---|---|---|
| Test reads the wrong email | Shared inbox or weak matching | Create a unique inbox and match by timestamp, sender, and subject |
| Email arrives after timeout | Slow app queue or short wait window | Use bounded polling with realistic timeout and useful failure logs |
| Retry passes using old email | Reused address across attempts | Create a new inbox per retry or filter by attempt ID |
| Parser breaks after template change | Fragile HTML scraping | Extract from structured JSON and validate expected URL or code patterns |
| Parallel jobs interfere | Same address used across jobs | Scope inboxes to job, test case, or attempt |
| Webhook event is spoofed | No signature verification | Verify signed payloads before acting on webhook events |
If you need a broader framework for deciding when this pattern is worth adding, Mailhook also explains when to use a burner email address in automation across signup, QA, and agent workflows.
A practical CI checklist
Before merging an email dependent test into your main pipeline, confirm these points:
- The inbox is created by API during the run
- The email address is unique enough for your CI parallelism model
- The test stores the inbox ID separately from the address
- Retrieval uses JSON through polling or webhooks
- The message matcher checks sender, subject, timestamp, and run context
- Verification links or codes are redacted from logs
- Retries cannot consume a previous attempt’s email
- CI failures include enough diagnostics to debug delivery or parsing
This checklist is intentionally short. The more complicated your email test harness becomes, the more likely it is to fail for reasons unrelated to your product.
Where Mailhook fits
Mailhook is designed for programmable disposable inboxes rather than manual temporary mail. For CI runs, that means your pipeline can create inboxes through an API, receive messages as structured JSON, and choose between webhook notifications and polling. For teams building AI agents, the JSON output is also easier to pass into tool calls than a visual inbox.
Because Mailhook supports instant shared domains, you can start without domain setup. If your CI environment needs a branded or isolated receiving domain, custom domain support is available. For webhook based flows, signed payloads help verify that received events are authentic. And since Mailhook does not require a credit card to get started, it is practical to test the workflow before committing it to your main pipeline.
Frequently Asked Questions
What is a burner email address for CI? A burner email address for CI is a temporary, disposable email address created during an automated test run. It lets your pipeline receive verification emails, invite messages, or login codes without using a shared human mailbox.
Should I create one burner email address per run or per test? Use one per run for simple serial pipelines. Use one per test case or retry attempt when tests run in parallel, when stale messages cause flakes, or when multiple email flows happen in the same suite.
Is polling or webhooks better for CI email tests? Webhooks are fast when your test environment can receive inbound events. Polling is easier in many hosted CI systems. Both can be reliable if the email data is structured and the test uses strict matching.
Can an LLM agent handle email verification in CI? Yes, but keep deterministic checks in code. Let the agent coordinate steps if needed, while your test helper creates the inbox, parses JSON, validates domains, extracts codes, and decides pass or fail.
Why not use a normal temporary inbox website? Browser based temporary inboxes are usually built for humans, not CI. They often require UI scraping, reuse public inboxes, and break under parallelism. CI needs API access, JSON payloads, and isolated inboxes.
Build email verification into CI without mailbox flakes
To create a burner email address for CI runs reliably, make the inbox part of your test infrastructure. Create it through an API, scope it to the run or attempt, receive the message as JSON, and parse only the verification value your test needs.
Mailhook gives developers and agent builders programmable disposable inboxes for exactly this kind of workflow. Start with an instant shared domain, move to a custom domain when needed, and use polling or signed webhooks depending on how your CI environment is built.
You can explore the API first approach at Mailhook and use it to replace shared inbox flakes with deterministic email automation.