mailcapture
Official JavaScript/TypeScript SDK for MailCapture — a real email capture API for integration testing OTP codes, verification links, and other transactional emails.
MailCapture captures emails sent by your application during testing. Point your app at a MailCapture address, trigger your email flow, then use this SDK to retrieve and assert on what arrived — subject lines, body text, OTP codes, and more. No mock SMTP server, no inbox polling, no flaky waits.
A MailCapture account is required — free and paid plans are available. Sign up at mailcapture.app.
Installation
npm install mailcapture
# or
yarn add mailcapture
# or
pnpm add mailcaptureRequires Node.js 18+ (uses native fetch).
Quick start
import MailCapture from 'mailcapture'
const mc = new MailCapture(process.env.MAILCAPTURE_API_KEY)
// Get your capture address template
const { username } = await mc.ping()
// Send an email to username-signup@mailcapture.app, then:
const email = await mc.waitFor('signup', { timeout: 15_000 })
console.log(email.subject) // "Welcome to Acme!"
console.log(email.otp) // "123456" — extracted automaticallyThe pattern for integration tests
- Clear the inbox before each test
- Trigger the action that sends the email (register, reset password, etc.)
- Wait for the email —
waitForholds the connection open and returns the instant it arrives - Assert on subject, OTP, body, links
- Clean up after
// vitest / jest example
describe('signup flow', () => {
let mc: MailCapture
let inbox: Inbox
beforeAll(async () => {
mc = new MailCapture(process.env.MAILCAPTURE_API_KEY)
await mc.ping() // validates key, caches username
inbox = mc.inbox('signup')
})
beforeEach(() => inbox.clear()) // clean slate for every test
it('sends a 6-digit OTP on signup', async () => {
await registerUser(inbox.address) // e.g. "alice-signup@mailcapture.app"
const email = await inbox.waitFor({ timeout: 10_000 })
expect(email.subject).toBe('Verify your email')
expect(email.otp).toMatch(/^\d{6}$/)
})
})API reference
new MailCapture(apiKey, options?)
const mc = new MailCapture('mc_...')
// With options
const mc = new MailCapture('mc_...', {
baseUrl: 'http://localhost:3002', // for local dev
requestTimeout: 15_000, // ms, default 10_000
})mc.ping() → PingResult
Validates your API key and returns your capture address template. Also caches your username internally so mc.address(tag) works without a network call.
const { username, address_template, example } = await mc.ping()
// username: "alice"
// address_template: "alice-{tag}@mailcapture.app"
// example: "alice-signup@mailcapture.app"mc.waitFor(tag, options?) → Capture
Long-polls the API and returns the first email captured for the given tag. This is efficient — the server holds the connection open and responds the moment an email arrives. No polling delay.
const email = await mc.waitFor('signup', {
timeout: 15_000, // total wait time in ms (default: 30_000)
pollTimeout: 10, // per-poll server timeout in seconds (default: 10, max: 30)
after: new Date(), // only emails received after this (default: 60s ago)
})Throws MailCaptureTimeoutError if no email arrives before timeout.
mc.inbox(tag) → Inbox
Returns a scoped Inbox object for a specific tag. Cleaner than passing the tag to every call.
const inbox = mc.inbox('password-reset')
inbox.address // "alice-password-reset@mailcapture.app" (requires ping() first)
await inbox.waitFor() // same as mc.waitFor('password-reset')
await inbox.list() // same as mc.list({ tag: 'password-reset' })
await inbox.clear() // same as mc.delete('password-reset')mc.address(tag) → string
Generates the capture email address for a tag. Synchronous — requires ping() to have been called first.
await mc.ping()
mc.address('signup') // "alice-signup@mailcapture.app"mc.list(options?) → CaptureList
List recent captures (newest first).
const { items, count } = await mc.list({
tag: 'signup', // filter by tag
limit: 25, // default 25, max 100
after: someDate, // only captures after this date
})mc.get(id) → Capture
Get a single capture by ID.
const email = await mc.get('e0f5922d-d8a9-4b03-bc60-9507b2e2f665')Throws MailCaptureNotFoundError if not found.
mc.delete(tag) → void
Delete all captures for a tag. Use this before each test for a clean starting state.
await mc.delete('signup')
// or via inbox:
await inbox.clear()The Capture object
interface Capture {
id: string // UUID
tag: string // e.g. "signup"
subject: string // email subject line
otp: string | null // extracted OTP/code, if detected
body_text: string | null // plain text body
body_html: string | null // HTML body
latency_ms: number // time from send to capture, in ms
status: string // e.g. "captured"
received_at: string // ISO 8601 timestamp
}The otp field is automatically extracted from the email body. If your OTP is embedded in a sentence, the service finds it for you. If no code is detected, it's null.
Error handling
All errors extend MailCaptureError and have a .code property.
import {
MailCaptureAuthError,
MailCaptureTimeoutError,
MailCaptureNotFoundError,
MailCaptureNetworkError,
MailCaptureApiError,
} from 'mailcapture'
try {
const email = await mc.waitFor('signup', { timeout: 10_000 })
} catch (err) {
if (err instanceof MailCaptureTimeoutError) {
// No email arrived in time
console.error(`Waited ${err.waitedMs}ms for tag "${err.tag}"`)
console.error('Did the email send? Check your email service logs.')
} else if (err instanceof MailCaptureAuthError) {
// Invalid or revoked API key
console.error('Check your MAILCAPTURE_API_KEY')
} else if (err instanceof MailCaptureNetworkError) {
// Could not reach the API
console.error('Network error:', err.cause)
} else {
throw err
}
}| Error class | .code |
When |
|---|---|---|
MailCaptureAuthError |
UNAUTHORIZED |
Invalid or revoked API key |
MailCaptureTimeoutError |
TIMEOUT |
waitFor exceeded its timeout |
MailCaptureNotFoundError |
NOT_FOUND |
get(id) — capture doesn't exist |
MailCaptureNetworkError |
NETWORK_ERROR |
Could not reach the API |
MailCaptureApiError |
varies | Unexpected API error |
Running tests against a local server
const mc = new MailCapture('mc_your_key_here', {
baseUrl: 'http://localhost:3002',
})Parallel tests
Each tag is its own inbox — safe to run in parallel.
const [signupEmail, resetEmail] = await Promise.all([
mc.waitFor('signup', { timeout: 15_000 }),
mc.waitFor('password-reset', { timeout: 15_000 }),
])Just make sure your test tags are unique per test run if tests share an API key.
Environment variable
The SDK reads no environment variables automatically. Pass your key explicitly:
const mc = new MailCapture(process.env.MAILCAPTURE_API_KEY!)Get your API key at mailcapture.app/admin/api-keys.