- Home
- Email testing
Test transactional email from your code
Test transactional email with a real inbox for each run. Assert message content, check signup confirmations, follow passwordless links, or retrieve email codes through the API.
No credit card required.
How it works
From email trigger to content assertion
The same four steps whether you're testing locally or in CI.
- 1
Isolate each run
Create an inbox on an approved receiving domain. Unique addresses keep parallel tests from reading each other's messages; stay within your plan limits.
- 2
Trigger an email
Exercise your app's signup, password reset, notification, or receipt flow with Playwright or your existing test runner.
- 3
Wait for delivery
Use a bounded retry while the message is in transit. A successful send request alone does not prove that the email reached the inbox.
- 4
Assert the contract
Check the expected subject and message content, then use a dedicated workflow below for codes, sign-in links, or account verification.
Integration example
Check a welcome email with bounded retries
An illustrative content check, not an OTP flow. Adapt the trigger, expected subject, and message search to your application.
Illustrative integration templates: these examples have not been executed against your app.Install the linked SDK and Playwright test runner (or pytest-playwright for Python), install a Playwright browser, and set PI_API_KEY in your environment. Use an approved receiving domain and a key with email_inboxes:create and email_messages:read for tests that create inboxes. Replace URLs, selectors, search terms, and assertions with your application contract. Keep credentials out of source control, use unique addresses, and delete test inboxes after runs with a separately authorized cleanup key.
import { test, expect } from '@playwright/test'
import { randomUUID } from 'node:crypto'
import { Configuration, EmailInboxesApi } from '@programmableinbox/sdk'
const api = new EmailInboxesApi(new Configuration({ accessToken: process.env.PI_API_KEY }))
test('signup delivers the expected welcome email', async ({ page }) => {
const inbox = await api.createEmailInbox({
createEmailInboxRequest: {
email: `test-${randomUUID()}@mail.programmableinbox.com`,
name: 'CI email content test',
},
})
await page.goto('https://staging.yourapp.com/signup')
await page.fill('#email', inbox.data.email)
await page.click('button[type=submit]')
await expect(async () => {
const result = await api.getEmailInboxMessages({
id: inbox.data.id, q: 'Welcome', limit: 1,
})
const message = result.data.messages[0]
expect(message).toBeDefined()
expect(message.subject).toContain('Welcome')
expect(message.bodyText).toContain('Thanks for signing up')
}).toPass({ timeout: 20_000, intervals: [1000, 2000, 3000] })
})import os
import time
from uuid import uuid4
import programmableinbox
from programmableinbox.api.email_inboxes_api import EmailInboxesApi
from playwright.sync_api import Page
configuration = programmableinbox.Configuration(access_token=os.environ["PI_API_KEY"])
def test_signup_delivers_the_expected_welcome_email(page: Page):
with programmableinbox.ApiClient(configuration) as api_client:
api = EmailInboxesApi(api_client)
inbox = api.create_email_inbox(create_email_inbox_request={
"email": f"test-{uuid4()}@mail.programmableinbox.com",
"name": "CI email content test",
})
page.goto("https://staging.yourapp.com/signup")
page.fill("#email", inbox.data.email)
page.click("button[type=submit]")
message = None
for attempt in range(10):
result = api.get_email_inbox_messages(id=inbox.data.id, q="Welcome", limit=1)
if result.data.messages:
message = result.data.messages[0]
break
if attempt < 9:
time.sleep(2)
assert message is not None, "Welcome email did not arrive within the retry budget"
assert "Welcome" in (message.subject or "")
assert "Thanks for signing up" in (message.body_text or "")FAQ
Common questions
- What does an inbox API test?
- It lets your test inspect received email and use its contents in the next step. Cover recipient isolation, expected content, and application behavior. These checks do not measure spam placement at Gmail or Outlook, or replace visual email-client testing.
- Why not use a Gmail or Outlook test account?
- Both providers offer APIs. A shared mailbox still needs authentication setup and careful message correlation during parallel runs. Separate test addresses make isolation explicit; choose the service and permissions that fit your test environment.
- Does retrieving messages wait for delivery?
- No. Read endpoints return what has already arrived. Retry with a bounded timeout and a targeted search, then fail clearly if the expected message never arrives. Do not retry indefinitely or mistake an old message for the current run.
- How do I choose a workflow?
- Use email OTP testing for typed codes, magic-link testing for passwordless login URLs, and email verification testing for account confirmation. The linked guides distinguish these assertions rather than treating every email as an OTP.
- Can I run these checks in CI or self-host?
- Use an API key stored as a CI secret and an app that can send email to the receiving domain. Programmable Inbox is AGPL v3 open source and self-hostable; self-hosting also requires working inbound mail infrastructure.
Choose the next email workflow
- Email OTP testing
Use extracted email codes when the user types a code into a form.
- Magic-link testing
Test a passwordless sign-in URL in a fresh browser session.
- Email verification testing
Confirm signup changes the account from unverified to verified.
- AI agents
Let an MCP-connected agent read test messages with scoped permissions.
- Email automation
Route matching incoming messages to your downstream webhook workflow.
Spin up your secondary inbox
Create a programmable address, grab your first OTP, and wire up a rule in minutes. Self-host for free, or sign up for managed cloud — either way, you own your data.
No credit card required · AGPL v3 license · Community supported