MailFlatDocs
Documentation/API References/Recipes

Recipes

Complete, copy-paste patterns for the things people actually build with MailFlat.

A fresh inbox per test (pytest)

A fixture that opens an isolated inbox, hands the address to the test, and deletes it afterwards, even when the test fails.
pytest
# conftest.py, one throwaway inbox per test
import os, time, uuid, pytest, requests
API = "https://mailflat.net/api/v1"
H = {"X-API-Key": os.environ["MAILFLAT_API_KEY"]}
@pytest.fixture
def inbox():
prefix = f"ci-{uuid.uuid4().hex[:8]}"
r = requests.post(f"{API}/inboxes", headers=H,
json={"prefix": prefix, "retention_hours": 2})
address = r.json()["address"]
yield address
requests.delete(f"{API}/inboxes/{address}", headers=H)
def wait_for_otp(address, timeout=60):
deadline = time.time() + timeout
while time.time() < deadline:
email = requests.get(f"{API}/inboxes/{address}/latest", headers=H).json().get("email")
if email and email.get("otp_code"):
return email["otp_code"]
time.sleep(3)
raise AssertionError("no OTP arrived")
def test_signup_flow(inbox):
my_app.register(email=inbox) # your app sends the code
assert wait_for_otp(inbox) is not None
Line by linewhat each step of this pytest example does
uuid4().hex[:8]
A unique prefix per test, so parallel workers never read each other's mail.
yield address
Everything after the yield runs as teardown, including on failure.
retention_hours: 2
Safety net: even if teardown is skipped, the messages expire on their own.
raise AssertionError
Fail loudly on timeout. A silent None turns into a confusing error three lines later.

End-to-end signup (Playwright)

TypeScript
// e2e/signup.spec.ts, real address, real inbox, no mocking
import { test, expect } from "@playwright/test";
async function waitForOtp(address, timeoutMs = 60_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const r = await fetch(`${API}/inboxes/${address}/latest`, { headers: H });
const otp = (await r.json())?.email?.otp_code;
if (otp) return otp;
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error("OTP never arrived");
}
test("user can sign up with a real code", async ({ page }) => {
const address = await createInbox();
await page.goto("/signup");
await page.fill("#email", address);
await page.click("#submit");
await page.fill("#code", await waitForOtp(address));
await expect(page).toHaveURL(/dashboard/);
});
Line by linewhat each step of this TypeScript example does
Date.now().toString(36)
Cheap unique prefix, no extra dependency needed.
?.email?.otp_code
latest returns { email: null } until the first message lands.
await page.fill("#code", …)
The real code from a real email, with no test-mode backdoor in your app.
DELETE at the end
Keeps the agent inbox list clean between runs.

Wire it into CI (GitHub Actions)

workflow.yml
# .github/workflows/e2e.yml
name: e2e
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test
env:
MAILFLAT_API_KEY: ${{ secrets.MAILFLAT_API_KEY }}
# wipe anything the run left behind
- if: always()
run: |
curl -X DELETE "https://mailflat.net/api/agent/inboxes?group=ci" \
-H "Authorization: Bearer ${{ secrets.MAILFLAT_TOKEN }}" || true
Line by linewhat each step of this workflow.yml example does
secrets.MAILFLAT_API_KEY
Repository secret. An account key in a public log grants access to every inbox you own.
if: always()
Runs the sweep even when the tests failed. That is exactly when inboxes get left behind.
DELETE ?group=ci
Tears down the whole ci batch in one call. Guarded server-side: it only touches API-created inboxes you own.
Two different credentials
The test run uses an account key (X-API-Key) for /api/v1. The group teardown route lives on the session API, so it wants a session token. Store whichever you use as a CI secret, never in the repo.

Give an AI agent its own inbox

LangChain
# an AI agent that can sign itself up somewhere
pip install "mailflat[langchain]"
from mailflat.langchain import MailFlatToolkit
tools = MailFlatToolkit(api_key=os.environ["MAILFLAT_API_KEY"]).get_tools()
agent = create_react_agent(llm, tools, prompt)
agent.invoke({"input":
"Open an inbox, register on staging.example.com with it, "
"wait for the verification code and complete the signup."})
Line by linewhat each step of this LangChain example does
get_tools()
Returns six LangChain tools: create, list, read, wait_for_otp, send, delete_inbox. delete_message is MCP-only.
"Open an inbox…"
The model decides when to call each tool; you do not orchestrate the polling loop yourself.
Inside Claude Desktop or Cursor, use the MCP server instead, with the same tools and zero code. See Agent API & MCP.