Mailwurf

@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/e2e

Configure

import { createMailwurf } from "@mailwurf/e2e";

const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!); // API key (mw_live_…) from Dashboard → API keys

The 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:

.env.test
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

CallHTTP
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

ThrownWhen
MailwurfTimeoutErrorHTTP 408: wait ended without a match
MailwurfErrorAny other non-2xx response. status holds the HTTP status, message the API error.
Original errorNetwork 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;
  }
}

On this page