MailFlatDocs
Documentation/Languages/JavaScript

Email testing with JavaScript

Zero dependencies, built on the runtime's own fetch. Drop it into Jest, Vitest, Playwright, Cypress or a Node script.

Before you begin

  1. A MailFlat account
    Free to start, no card. Every plan can open inboxes from the API.
  2. An account API key
    Agents → API keys in the dashboard. It looks like mf_live_… and goes in the X-API-Key header. Keep it in an environment variable, never in the repo. See API keys and authentication.
  3. Node 18+ (or any fetch-capable runtime)
    That is the whole toolchain requirement.

Install

JavaScript
npm i @mailflat/sdk
Package: @mailflat/sdk

Your first inbox and one-time code

The whole loop: open an address, let your app mail it, read the code back, clean up.
JavaScript
import { MailFlat } from "@mailflat/sdk";
 
const mf = new MailFlat({ apiKey: process.env.MAILFLAT_API_KEY });
 
// 1. a real, deliverable address
const inbox = await mf.create({ label: "signup" });
console.log(inbox.address); // signup@a7f2c.mailflat.net
 
// 2. your app sends the code to that address
await myApp.register({ email: inbox.address });
 
// 3. read it back, no mocking anywhere
const otp = await inbox.waitForOtp({ timeout: 30000 });
console.log(otp); // "482913"
 
// 4. done with it
await inbox.delete();
Line by linewhat each step of this JavaScript example does
create(...)
Returns immediately with a real, deliverable address. Nothing is queued or simulated.
retention_hours
Optional. Messages purge themselves after it, so a skipped teardown never leaks.
wait_for_otp
Polls for you and fails loudly on timeout, instead of returning nothing three lines before the real error.
delete()
Optional but tidy. Retention would clean up anyway.
The address is real
Mail actually travels: SMTP, DKIM, the lot. Nothing is stubbed, so a broken template or a misconfigured sender fails here exactly like it would in production.

Waiting for the code

We extract the one-time code server-side and hand it to you as a field, so you never write a regex against an email body.
JavaScript
// timeout and pollInterval are MILLISECONDS here (the Python client uses seconds)
const otp = await inbox.waitForOtp({ timeout: 30000, pollInterval: 1000 });
 
// need the whole message?
const msg = await inbox.waitForMessage({ timeout: 30000 });
console.log(msg.subject, msg.sender, msg.text);
 
// both reject on timeout, so a missing email fails the test loudly
import { OTPTimeoutError } from "@mailflat/sdk";
try {
await inbox.waitForOtp({ timeout: 5000 });
} catch (err) {
if (err instanceof OTPTimeoutError) console.log("no code arrived");
}
Always set a timeout
A poll loop without a deadline turns a missing email into a hung job. Fail loudly instead: the error message should name the address you were waiting on.

Reading every message

JavaScript
for (const msg of await inbox.messages()) { // newest first
console.log(msg.subject, msg.sender, msg.receivedAt);
console.log(msg.text); // plain text body
console.log(msg.html); // HTML body
console.log(msg.otp); // extracted code, or undefined
}
 
const latest = await inbox.latest(); // null until the first message lands
if (latest?.subject?.includes("Reset")) {
await latest.delete(); // drop one message, keep the inbox
}
FieldTypeWhat it is
subjectstringSubject line
senderstringFrom address
body_textstringPlain text body
body_htmlstringHTML body
otp_codestring | nullOne-time code, extracted by us
to_addressstringThe exact address it was sent to, tag included
tagstring | nullPlus-addressing tag, if the sender used one
received_atISO 8601When it landed
is_encryptedbooleanTrue on end-to-end encrypted inboxes, where body and code are unavailable

Sending mail from the inbox

Useful in reverse: point your own inbound pipeline at a MailFlat address and check that it handles what arrives.
JavaScript
// mail leaves from the inbox address, DKIM-signed by our own MTA
await inbox.send("someone@example.com", {
subject: "Welcome",
body: "Plain text body",
html: "<p>Optional HTML body</p>",
});

Cleaning up

JavaScript
await inbox.delete(); // inbox and every message, immediately
 
// or leave it: messages expire on their own at the retention you asked for
const inbox = await mf.create({ label: "ci", retentionHours: 2 });
Two safety nets, use both
Delete in teardown so the list stays readable, and set retention_hours so a crashed run still cleans itself up.

In a test suite

Works with Jest, Vitest, Playwright, Cypress and anything else that gives you a setup and teardown hook.
JavaScript
// tests/signup.spec.js — one isolated inbox per test, torn down even on failure
import { test, expect } from "@playwright/test";
import { MailFlat } from "@mailflat/sdk";
 
const mf = new MailFlat({ apiKey: process.env.MAILFLAT_API_KEY });
 
test("user can sign up with a real code", async ({ page }) => {
const inbox = await mf.create({
prefix: `ci-${Date.now().toString(36)}`,
retentionHours: 2,
});
 
try {
await page.goto("/signup");
await page.fill("#email", inbox.address);
await page.click("#submit");
 
await page.fill("#code", await inbox.waitForOtp({ timeout: 60000 }));
await expect(page).toHaveURL(/dashboard/);
} finally {
await inbox.delete();
}
});
Line by linewhat each step of this JavaScript example does
unique prefix
One inbox per test, so parallel workers never read each other's mail.
teardown hook
Runs even when the test fails — that is exactly when inboxes get left behind.
real code, real email
No test-mode backdoor in your app: the path under test is the one your users take.

JavaScript reference

MethodWhat it does
mf.create({ prefix, label, subdomain, domain, retentionHours })Open an inbox, returns Inbox
mf.list()Every inbox this key opened
mf.inbox(address)Attach to an existing address without an API call
inbox.waitForOtp({ timeout, pollInterval })Poll until a code arrives, rejects with OTPTimeoutError
inbox.waitForMessage({ timeout })Same, but resolves to the whole Message
inbox.messages() / inbox.latest()All messages (newest first) / the newest one or null
inbox.send(to, { subject, body, html })Send from this address, DKIM-signed
inbox.delete() / inbox.deleteMessage(id)Drop the inbox / one message
Full endpoint reference, including error shapes and rate limits: Agent API and MCP.

Same flow, another language