E2E Testing: How to Read a Confirmation Email Automatically
To read a confirmation email in an E2E test, give the test its own inbox, trigger your staging signup, poll for the specific new message, and extract the confirmation link only after checking its recipient, sender, subject, and freshness. Then open the allowed staging URL and assert the account actually became verified. A signup HTTP 200 proves none of those later steps.
This guide separates delivery from authentication, with an Inboxto-specific quota worksheet and a small Playwright pattern. It is an integration design, not a claim that we ran a real signup or delivered mail today.
1. Define the evidence before sending
A useful test has four checkpoints: the application accepted the signup, the expected email reached the assigned inbox, the URL belongs to your staging application, and the account changed to the intended verified state. A failure at one checkpoint should not be reported as a failure at another.
Provision one address per test attempt, including retries and parallel workers. Use an address on a domain your team controls or is authorized to use. A unique local part is not enough if every worker still reads the same unfiltered inbox. Record a baseline of message IDs before triggering signup; a resent message from yesterday must not satisfy today's assertion.
For Inboxto, the public API documentation currently describes GET /v1/messages?address=...&limit=20 and GET /v1/messages/:id under https://inboxto.app/api, both requiring messages:read. Responses wrap results in data; documented message fields include id, to, from, subject, received_at, text_body, and html_body. Keep your Bearer key in the CI secret store and the mailbox-reading helper in the Node test process, not browser code. API access is currently a Premium feature; the free UI remains useful for manual checks.
The developer overview explains the wider setup. Confirm the live schema rather than copying an old response shape into a new adapter.
2. Poll a bounded, correlated inbox
Playwright's assertions guide supports expect.poll for retrying asynchronous checks. Set its timeout and intervals explicitly: the usual short assertion timeout is not an email-delivery guarantee. With Cypress, use a Node-side mail helper or task and an explicit bounded polling loop; cy.request documentation says chained assertions run once, not as an automatic inbox polling system.
Here is a Playwright orchestration sketch. The mail methods are your server-side adapter, not an Inboxto SDK. list returns the documented data array; get returns one data object. Each method must enforce its own request timeout, validate the response shape, and throw on authentication errors, exhausted quota, or network failure. Do not turn every failure into an empty inbox.
// Inside a Playwright test; provision address separately.
test.setTimeout(90_000);
const baseline = new Set((await mail.list(address)).map(m => m.id));
await triggerOwnedStagingSignup(address);
let reads = 0;
let candidate;
await expect.poll(async () => {
if (++reads > 5) throw new Error('Inbox read budget exhausted');
const messages = await mail.list(address);
const matches = messages.filter(m =>
!baseline.has(m.id) && m.to === address &&
m.from === expectedSender && m.subject === expectedSubject
);
if (matches.length > 1) throw new Error('Ambiguous confirmation mail');
candidate = matches[0];
return Boolean(candidate);
}, { timeout: 60_000, intervals: [3_000, 8_000, 15_000, 25_000] })
.toBe(true);
const message = await mail.get(candidate.id);
expect(message.id).toBe(candidate.id);
expect(message.to).toBe(address);
expect(message.from).toBe(expectedSender);
expect(message.subject).toBe(expectedSubject);
const confirmationUrl = extractAllowedUrl(message.html_body);
await page.goto(confirmationUrl);
await expect(page.getByTestId('account-verified')).toBeVisible();
Import test and expect from @playwright/test in your test file and replace every application-specific helper and locator. The enclosing test budget must cover setup, polling, detail read, navigation, and the final assertion; this example uses 90 seconds. Give individual requests and remaining steps explicit deadlines within that budget. Five polling reads are a cap, not a promise that all five fit in 60 seconds; request time counts too. Set per-request deadlines shorter than the remaining overall budget. A 401/403 is a configuration failure. On 429, respect the provider's rate-limit/reset information and stop or defer the test within your CI budget; do not hammer the endpoint.
For freshness, use the unique address and baseline together. Where your own mail template supports it, include a non-secret run marker and match it too. Inspect received_at for diagnostics, but do not rely only on a timestamp comparison across unsynchronized clocks. For OTP and browser-specific details, see our Playwright and Cypress testing guide.
3. Extract a URL without executing the email
Parse html_body in the Node helper using an already-installed, non-rendering HTML parser that does not fetch external resources or execute scripts. A detached browser DOMParser document is not a no-network guarantee: resource-bearing elements can still cause downloads. Never insert email HTML into the live application DOM or render its remote resources. Read anchor href attributes, resolve them with new URL, then require HTTPS, the exact configured staging origin, the expected confirmation pathname, and no embedded username or password. Reject zero or multiple qualifying links. A substring check such as includes('staging.example') accepts attacker-controlled hostnames.
Some templates use an email provider's click-tracking redirect. Do not broaden the allowlist to every redirect host just to make the test pass. For your controlled staging template, disable tracking on authentication links or validate the documented redirect flow separately. Do not silently follow a URL from an unexpected sender or domain.
The Node helper should return the validated URL without printing it. It is a bearer capability while valid. Mask it in traces and failure artifacts as well as ordinary console logs. Sender and subject matching correlate the fixture; they are not cryptographic proof of sender identity. Your staging trust boundary and final state assertion still matter.
If opening the link shows only a generic success banner, also assert a protected account view or read the authorized test API for that account's verified state. Delivery success is not signup success. For the broader workflow, read automating email verification.
4. Budget quota and preserve useful evidence
Inboxto editorial worksheet, October 25, 2026: the current docs state a default 100 API requests per hour. One baseline list, up to five polling lists, and one detail read make seven reads per attempt. Ten attempts can therefore consume 70 reads before address provisioning, cleanup, retries, or other jobs. This is planning arithmetic, not a load test or guaranteed capacity. Share the budget across workers and honor actual rate-limit headers.
Our product-specific debugging choice is deliberately non-destructive: use list plus detail, not POST /v1/messages/pop, which reads and deletes a message. A persistent Inboxto address lets you return to the same inbox while you retain authorized access, but it does not preserve every email indefinitely. The privacy policy currently lists ordinary mail retention of seven days on Free and 90 days on Premium. Free has five starred-message slots; Premium has unlimited starred messages. Starred mail remains until manual deletion or account closure.
Save a sanitized fixture or redact sensitive content before sharing a failure. Star a useful non-secret diagnostic message only when appropriate; keeping a live confirmation token longer is not inherently better. A persistent address does not extend the sender's token lifetime or guarantee account recovery.
| Observation | Next check |
|---|---|
| No matching message | Queue/provider acceptance, exact recipient, remaining quota |
| Several matches | Address isolation, retries, correlation marker |
| Message found, URL rejected | Template origin/path and tracking configuration |
| URL opened, account unverified | Expiry, one-time use, browser binding, application state |
5. Clean up without destroying the explanation
After success, remove your own staging account and mail according to the test's retention policy. After failure, preserve only the minimum restricted evidence needed to diagnose it, with an expiry and an owner. Do not delete a shared inbox or every message in an account. If reusing an address intentionally for recovery testing, retain the baseline and isolate concurrent attempts.
Inboxto receives email; it does not send your application's confirmation mail or provide SMS verification. Domain acceptance and delivery remain external dependencies. No bypassing platform restrictions or testing someone else's account is part of this workflow.
Start with one owned staging signup. Open Inboxto for manual inspection, then connect a least-privilege API key when automation is justified. A trustworthy E2E test explains which boundary failed, rather than turning every green HTTP response into a green signup.