Skip to main content
Stably’s email inbox lets you receive emails during tests and extract structured data like OTPs and magic links. Each inbox is scoped to your test, preventing interference between parallel runs.
Why use email inbox? Testing email-based flows (sign-up verification, password reset, magic links) requires receiving real emails. The inbox handles email delivery, filtering, and AI-powered extraction so you can focus on testing your application.

Installation

Install the @stablyai/email package:
import { Inbox } from '@stablyai/email';
Works with any test framework — Playwright, Jest, Vitest, Cypress, or backend scripts.

Email Object

Methods like inbox.listEmails(), inbox.waitForEmail(), and inbox.getEmail() return Email objects with these properties:
PropertyTypeDescription
idstringUnique email identifier
mailboxstringThe mailbox containing this email (e.g., "INBOX")
from{ address: string, name?: string }Sender
to{ address: string, name?: string }[]Recipients
subjectstringEmail subject line
receivedAtDateWhen the email was received
textstring?Plain text body (if available)
htmlstring[]?HTML body parts (if available)

Basic Usage

Create an inbox, trigger an email from your app, wait for it to arrive, and extract the data you need:
import { Inbox } from '@stablyai/email';
import { z } from 'zod';

test('login with OTP', async ({ page }) => {
  const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });

  await page.goto('/login');
  await page.fill('#email', inbox.address);
  await page.click('#send-otp');

  const email = await inbox.waitForEmail({ subject: 'verification' });
  const { data: otp } = await inbox.extractFromEmail({ id: email.id, prompt: 'Extract the OTP code' });

  await page.fill('#otp', otp);
  await page.click('#verify');

  await inbox.deleteAllEmails();
});

Inbox.build

Creates an inbox instance scoped to your test. Requires STABLY_API_KEY and STABLY_PROJECT_ID environment variables (or pass them directly).
const inbox = await Inbox.build();
// inbox.address → "[email protected]"

const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
// inbox.address → "[email protected]"

// Explicit credentials (overrides environment variables)
const inbox = await Inbox.build({
  suffix: `test-${Date.now()}`,
  apiKey: 'your-api-key',
  projectId: 'your-project-id',
});

Options

OptionTypeDescription
suffixstringSuffix for test isolation (e.g., "test-123" creates "[email protected]")
apiKeystringStably API key. Defaults to STABLY_API_KEY environment variable
projectIdstringStably project ID. Defaults to STABLY_PROJECT_ID environment variable
Using a unique suffix for each test is highly recommended. It ensures tests running in parallel don’t interfere with each other by giving each test its own isolated inbox address.

Inbox Properties

PropertyTypeDescription
addressstringFull email address (with suffix if provided)
suffixstring | undefinedThe suffix used when creating the inbox
createdAtDateWhen the inbox was created (used for filtering)
The inbox automatically filters out emails received before createdAt, so you only see emails from your current test.

inbox.waitForEmail

Polls until a new email matching your filters arrives. Only sees emails received after the inbox was created.
const email = await inbox.waitForEmail({ subject: 'Welcome' });

const email = await inbox.waitForEmail({
  from: '[email protected]',
  subject: 'verification',
  timeoutMs: 60000,       // default: 120000 (2 min)
  pollIntervalMs: 5000,   // default: 3000 (3 sec)
});

Options

OptionTypeDescription
fromstringFilter by sender address
subjectstringFilter by subject (contains match)
subjectMatch'contains' | 'exact'Subject matching mode (default: 'contains')
timeoutMsnumberMax wait time in ms (default: 120000)
pollIntervalMsnumberPoll interval in ms (default: 3000)
Throws EmailTimeoutError if no matching email arrives within the timeout.

inbox.extractFromEmail

Extracts data from an email using AI. Returns { data, reason } where data is the extracted value and reason describes how the extraction was performed.
// String extraction - returns { data: string, reason: string }
const { data: otp, reason } = await inbox.extractFromEmail({ id: email.id, prompt: 'Extract the 6-digit OTP code' });
console.log(reason); // e.g., "Found a 6-digit code in the email body"

// Handling extraction failure
try {
  const { data: otp } = await inbox.extractFromEmail({ id: email.id, prompt: 'Extract the 6-digit OTP code' });
} catch (error) {
  if (error instanceof EmailExtractionError) {
    console.log(error.reason); // e.g., "No OTP code found in the email"
  }
}

// Structured extraction with Zod schema - returns { data: T, reason: string }
const { data } = await inbox.extractFromEmail({
  id: email.id,
  prompt: 'Extract the OTP code',
  schema: z.object({ otp: z.string() }),
});
console.log(data.otp); // typed string

// Multiple fields
const { data: order } = await inbox.extractFromEmail({
  id: email.id,
  prompt: 'Extract the verification URL and expiration time',
  schema: z.object({
    url: z.string().url(),
    expiresIn: z.string(),
  }),
});
console.log(order.url, order.expiresIn);
The method throws EmailExtractionError if extraction fails.

inbox.listEmails

Lists emails in the inbox. By default, only returns emails received after the inbox was created.
const { emails } = await inbox.listEmails();

// Include emails from before inbox creation
const { emails } = await inbox.listEmails({ includeOlder: true });

const { emails, nextCursor } = await inbox.listEmails({
  from: '[email protected]',
  subject: 'Order',
  limit: 10,
});

Options

OptionTypeDescription
fromstringFilter by sender address
subjectstringFilter by subject (contains match)
subjectMatch'contains' | 'exact'Subject matching mode (default: 'contains')
limitnumberMax emails to return (default: 20, max: 100)
cursorstringPagination cursor from previous response
sinceDateOverride the default createdAt filter
includeOlderbooleanSet true to include emails from before inbox creation

inbox.getEmail

Gets a specific email by ID.
const email = await inbox.getEmail(id);
console.log(email.subject, email.from.address);

inbox.deleteEmail / inbox.deleteAllEmails

Delete emails to clean up after your test.
await inbox.deleteEmail(email.id);  // Delete single email
await inbox.deleteAllEmails();       // Delete all emails in this inbox
deleteAllEmails() only deletes emails sent to this inbox’s address, not your entire org mailbox.

Common Extraction Prompts

// OTP / Verification code - simple string
const { data: otp } = await inbox.extractFromEmail({ id: email.id, prompt: 'Extract the OTP or verification code' });

// Magic link / Sign-in URL - simple string
const { data: url } = await inbox.extractFromEmail({ id: email.id, prompt: 'Extract the sign-in or verification URL' });

// Structured extraction with schema
const { data } = await inbox.extractFromEmail({
  id: email.id,
  prompt: 'Extract the OTP code',
  schema: z.object({ otp: z.string() }),
});
console.log(data.otp);

// Order confirmation - multiple fields
const { data: order } = await inbox.extractFromEmail({
  id: email.id,
  prompt: 'Extract the order number and estimated delivery date',
  schema: z.object({
    orderNumber: z.string(),
    deliveryDate: z.string(),
  }),
});
console.log(order.orderNumber, order.deliveryDate);

Using Fixtures

Create a reusable inbox fixture for automatic cleanup:
import { test as base } from '@playwright/test';
import { Inbox } from '@stablyai/email';

const test = base.extend<{ inbox: Inbox }>({
  inbox: async ({}, use, testInfo) => {
    const inbox = await Inbox.build({ suffix: `test-${testInfo.testId}` });
    await use(inbox);
    await inbox.deleteAllEmails();
  },
});

test('signup flow', async ({ page, inbox }) => {
  await page.fill('#email', inbox.address);
  await page.click('#signup');

  const email = await inbox.waitForEmail({ subject: 'Welcome' });
  // ...
});

Troubleshooting

Tests interfere with each other Use unique suffixes with Inbox.build({ suffix: 'test-' + Date.now() }) to give each test its own isolated inbox. The inbox automatically filters to only show emails received after it was created.