@mailwurf/e2e
Fetch-only TypeScript package for the mailbox API. Playwright is a consumer, not a dependency.
@mailwurf/e2e wraps the HTTP API. It has no Playwright dependency. Anything that can fetch can use it: Playwright, Vitest, Cypress tasks, a Node script.
Install
Public on npm. ESM-only, Node 18 or later.
npm install --save-dev @mailwurf/e2eConfigure
import { createMailwurf } from "@mailwurf/e2e";
const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!); // API key (mw_live_…) from Dashboard → API keysThe first argument is the organization key (mw_live_…). It is required: createMailwurf() throws when it is empty, so a missing key fails at startup instead of with 401 later. The package reads no environment variables itself. Keep the key wherever your runner keeps secrets, for example:
MAILWURF_API_KEY=mw_live_…The package talks to https://api.mailwurf.io and calls paths under /v1. Pass apiUrl only for a self-hosted Mailwurf. A host without scheme, such as mailwurf.example.com, is treated as https://. Trailing slashes are ignored. fetch can be injected for unit tests.
const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!, {
apiUrl: "https://mailwurf.example.com",
});The key is sent as Authorization: Bearer. Create a key under API keys; the full value is shown once.
Usage
One inbox per test. The app keeps its real mail sender.
const inbox = await mailwurf.createInbox();
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Sign up" }).click();
const mail = await inbox.waitFor({ subject: "Confirm", timeout: 15_000 });
await page.goto(mail.links.find((url) => url.includes("/confirm"))!);
// mail.otp → "482193"subject is a case-insensitive substring, matched on the server. timeout is milliseconds, default 15000, maximum 120000. 0 is a single lookup with no wait. See Waiting.
Client surface
| Call | HTTP |
|---|---|
health() | GET /v1/health |
createInbox() | POST /v1/inboxes |
getInbox(id) | GET /v1/inboxes/{id} |
inboxByAddress(address) | GET /v1/inboxes?address= |
getMessage(id) | GET /v1/messages/{id} |
listMessages(id, { subject, after }) | GET /v1/inboxes/{id}/messages |
send(id, { to, subject, text, html, inReplyTo, attachments }) | POST /v1/inboxes/{id}/messages |
waitFor(id, { subject, timeout, after, signal }) | GET /v1/inboxes/{id}/messages/wait |
deleteInbox(id) | DELETE /v1/inboxes/{id} |
/api/v1 is an alias of /v1.
createInbox(), getInbox(), and inboxByAddress() return a MailwurfInbox with id, address, createdAt, expiresAt, plus waitFor(), send(), messages(), and delete(). When sending, from is always the inbox address. See Sending.
Temporary inboxes expire after one hour. Custom temporary addresses are not supported. Permanent addresses are managed in the dashboard and cannot be deleted through this package. Their expiresAt is null.
Message shape
Wire JSON is snake_case. The package maps to camelCase and Dates.
type Message = {
id: string;
inboxId: string;
from: string;
to: string;
subject: string;
text: string;
html: string;
links: string[];
otp: string;
receivedAt: Date;
};links is always an array, in order of appearance. otp is the first detected one-time code, else "". Wait does not filter on those fields. See Messages for how both are extracted.
Errors
| Thrown | When |
|---|---|
MailwurfTimeoutError | HTTP 408: wait ended without a match |
MailwurfError | Any other non-2xx response. status holds the HTTP status, message the API error. |
| Original error | Network failures, and aborts through signal |
MailwurfTimeoutError extends MailwurfError, so check for the timeout first.
import { MailwurfError, MailwurfTimeoutError } from "@mailwurf/e2e";
try {
await inbox.waitFor({ subject: "Confirm" });
} catch (error) {
if (error instanceof MailwurfTimeoutError) {
// no mail in time
} else if (error instanceof MailwurfError && error.status === 401) {
// key missing or revoked
} else {
throw error;
}
}
Mailwurf