Messages
What Mailwurf reads out of an email, and how to pick the link or code a test needs.
Every accepted email becomes one message. The wait and list calls return it already parsed.
type Message = {
id: string;
inboxId: string;
from: string;
to: string;
subject: string;
text: string;
html: string;
links: string[];
otp: string;
receivedAt: Date;
};Over HTTP the same fields are snake_case: inbox_id, received_at.
Fields
| Field | Contains |
|---|---|
from | The sending server's return address. With Resend or SES this is often a bounce address, not the visible sender. Mail sent from another Mailwurf inbox has that inbox's address. |
to | The inbox address. |
subject | Decoded subject line. |
text | Plain-text part. For HTML-only mail, a text version of the HTML. |
html | HTML part, or "". |
links | URLs found in the mail, in order of appearance, without duplicates. |
otp | The first one-time code found, or "". |
receivedAt | When Mailwurf accepted the mail. |
Attachments are not exposed.
Assert on subject, text, or html when the visible sender matters. Do not assert on from.
Links
links collects every href in the HTML first, then every http:// or https:// URL in HTML and text. That includes logo links, image URLs, mailto: links, and unsubscribe footers. Trailing ), ,, ., and ; are removed from each URL.
Pick the link by what it points to, not by position:
const mail = await inbox.waitFor({ subject: "Reset your password" });
const reset = mail.links.find((url) => url.includes("/reset-password"));
if (!reset) throw new Error(`no reset link in: ${mail.links.join(", ")}`);
await page.goto(reset.replaceAll("&", "&"));HTML entities in href values stay as written. A link with query parameters can contain &; replace it before opening the URL.
Tracking redirects (for example from click tracking in your mail provider) are not followed. Turn link tracking off for test environments, or match on the redirect domain.
One-time codes
otp is filled in this order:
- A code within 64 characters after one of the words
code,otp,pin,token, orpasscode(any case). A code is 4–8 digits, or 4–8 uppercase letters and digits with at least one of each. Words in between are skipped, so the code can sit on the next line. If another number comes first (a time, a date), that label is skipped and the next one is tried. A year such as2026right after a label is only used when no other step finds a code.otp,pin,token, andpasscodemust be whole words. - Otherwise, the first standalone 6-digit number.
- Otherwise, the first line that holds only a 6-character code of uppercase letters and digits.
- Otherwise
"".
URLs are ignored, so a ?code= parameter in a link is never read as the code. Each step checks the text part, then the visible HTML, before the next step runs.
Your verification code is 482193. → otp "482193"
Use PIN: 7731 to sign in. → otp "7731"
Your code: A7K2QX → otp "A7K2QX"
Code: 482 193 → otp ""Codes with spaces, dashes, or lowercase letters are not detected. Read them from text yourself:
const code = mail.text.match(/\b([A-Z0-9]{3}-[A-Z0-9]{3})\b/)?.[1];code also matches at the end of a longer word, so "Bestätigungscode" works but "barcode" can pick up an unrelated code. Inside a word ("encoded") it is not a label. If a template mixes numbers, assert on text directly.
Mailwurf