Mailloop provides a TypeScript SDK that lets you create sandboxes, send emails through them, and verify delivery programmatically. This is designed for automated test suites and CI/CD pipelines.
Getting an API Key
Go to your dashboard, open Settings > API Keys, and create a new key. Keys use the format ml_live_.... Store it as an environment variable:
export MAILLOOP_API_KEY=ml_live_your_api_key_here SDK Installation
# npm
npm install @mailloop/sdk
# yarn
yarn add @mailloop/sdk
# pnpm
pnpm add @mailloop/sdk Requires Node.js 18 or later. Zero dependencies.
Basic Pattern
The typical automated email test follows this flow:
- Create a temporary sandbox
- Configure your application to send through it
- Trigger the action that sends email
- Wait for the email to arrive
- Assert on the email content
import { MailloopClient } from '@mailloop/sdk';
const client = new MailloopClient({
apiKey: process.env.MAILLOOP_API_KEY
});
// 1. Create a temporary sandbox (auto-deletes after 60 seconds)
const sandbox = await client.sandboxes.createTemporary({
duration: 60
});
// 2-3. Your application sends email using sandbox.username / sandbox.password
// as SMTP credentials against sandbox.mailloop.io
// 4. Wait for the email
const email = await client.emails.waitFor(sandbox.id, {
subject: 'Welcome',
timeout: 30000
});
// 5. Assert
console.log(email.subject); // "Welcome to Acme"
console.log(email.html); // Full HTML body
console.log(email.to); // ["[email protected]"] Temporary Sandboxes
Temporary sandboxes auto-delete after a specified duration (1 second to 86400 seconds, i.e. up to 24 hours). They are ideal for test isolation because each test gets a clean, independent inbox that cleans itself up.
const sandbox = await client.sandboxes.createTemporary({
duration: 30,
name: 'signup-test'
});
// sandbox.isTemporary === true
// sandbox.deleteAfter === ISO timestamp For an inbox that has to outlive 24 hours, use persistent sandboxes and delete them manually:
const sandbox = await client.sandboxes.create({
name: 'e2e-tests'
});
// ... run tests ...
await client.sandboxes.delete(sandbox.id); Waiting for Emails
The waitFor method polls server-side and returns when a matching email arrives. No client-side polling loop needed.
// Wait for any email
const email = await client.emails.waitFor(sandbox.id);
// Wait for email from a specific sender
const email = await client.emails.waitFor(sandbox.id, {
from: '[email protected]'
});
// Wait for email with a subject substring match
const email = await client.emails.waitFor(sandbox.id, {
subject: 'Password Reset'
});
// Combine filters
const email = await client.emails.waitFor(sandbox.id, {
from: '[email protected]',
subject: 'Invoice',
timeout: 45000
}); The timeout must be between 1 and 60 seconds (default is 30 seconds); a value outside that range throws a ValidationError rather than being clamped. waitFor also matches an email that landed up to 15 seconds before the call, which absorbs the round trip and a slow write on the way in. If the send happened earlier in the run, raise sinceMs (0 to 900000); keep it tight in a sandbox you reuse across tests, because a wide window can match an older email that fits the same filter. If no matching email arrives, a TimeoutError is thrown.
Test Framework Examples
Vitest / Jest
import { describe, it, expect } from 'vitest';
import { MailloopClient } from '@mailloop/sdk';
const client = new MailloopClient({
apiKey: process.env.MAILLOOP_API_KEY
});
describe('signup emails', () => {
it('sends a welcome email after registration', async () => {
const sandbox = await client.sandboxes.createTemporary({
duration: 60
});
// Trigger your app to send email using sandbox SMTP credentials
await registerUser({
email: '[email protected]',
smtpHost: 'sandbox.mailloop.io',
smtpPort: 587,
smtpUser: sandbox.username,
smtpPass: sandbox.password
});
const email = await client.emails.waitFor(sandbox.id, {
subject: 'Welcome',
timeout: 15000
});
expect(email.subject).toContain('Welcome');
expect(email.to).toContain('[email protected]');
expect(email.html).toContain('Confirm your email');
});
}); Playwright (E2E)
import { test, expect } from '@playwright/test';
import { MailloopClient } from '@mailloop/sdk';
const client = new MailloopClient({
apiKey: process.env.MAILLOOP_API_KEY
});
test('password reset flow sends email with valid link', async ({ page }) => {
const sandbox = await client.sandboxes.createTemporary({
duration: 60
});
// Navigate to password reset page
await page.goto('/forgot-password');
await page.fill('[name="email"]', sandbox.emailAddress);
await page.click('button[type="submit"]');
// Wait for the reset email
const email = await client.emails.waitFor(sandbox.id, {
subject: 'Reset your password',
timeout: 30000
});
// Extract and follow the reset link
const body = email.html ?? email.text ?? '';
const resetLink = body.match(/href="([^"]*reset[^"]*)"/)?.[1] ?? body.match(/https?:\/\/\S*reset\S*/)?.[0];
expect(resetLink).toBeTruthy();
await page.goto(resetLink!);
await expect(page.locator('h1')).toContainText('New Password');
}); CI/CD Integration
GitHub Actions
name: Email Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
env:
MAILLOOP_API_KEY: ${{ secrets.MAILLOOP_API_KEY }} Store MAILLOOP_API_KEY as a repository secret in GitHub. The SDK reads it from process.env.MAILLOOP_API_KEY in your test setup.
Environment Variables
For any CI provider, set these environment variables:
| Variable | Value | Description |
|---|---|---|
MAILLOOP_API_KEY | ml_live_... | Your API key |
The SDK only needs the API key. SMTP credentials are generated per-sandbox at runtime.
Tips
Parallel Tests
Each test can create its own temporary sandbox, so tests run in parallel without interfering with each other:
// test-a.spec.ts -- gets its own sandbox
const sandbox = await client.sandboxes.createTemporary({ duration: 60 });
// test-b.spec.ts -- gets a different sandbox
const sandbox = await client.sandboxes.createTemporary({ duration: 60 }); Listing Emails
If you need to check multiple emails instead of waiting for a specific one:
let after: string | undefined;
let hasMore = true;
while (hasMore) {
const page = await client.emails.list(sandbox.id, { limit: 10, after });
for (const email of page.emails) {
console.log(`${email.from}: ${email.subject}`);
}
after = page.nextCursor ?? undefined;
hasMore = page.hasMore;
} after is a cursor into a newest-first list, so each page returns emails older than the previous one, and it takes precedence over offset. total is the sandbox-wide count on every page, including cursor pages, so use hasMore to decide when to stop.
Inspecting Full Email Details
The waitFor and get methods return the full email including HTML, text, headers, and attachment metadata:
const email = await client.emails.get(sandbox.id, emailId);
console.log(email.html); // Full HTML body
console.log(email.text); // Plain text body
console.log(email.headers); // All email headers
console.log(email.attachments); // Attachment metadata (filename, size, type) See the SDK Reference for the complete API.