# AI Coding Tools
Source: https://docs.zerodrop.dev/ai-coding
Use ZeroDrop with Cursor, GitHub Copilot, and Claude Code — TypeScript and Python.
# AI Coding Tools
> Use ZeroDrop with Cursor, GitHub Copilot, and Claude Code
If you use an AI coding assistant to write tests, add the ZeroDrop context file to your project. Your AI will automatically know how to write email tests using ZeroDrop instead of MailHog or mocking — in both TypeScript and Python.
## Cursor (.cursorrules)
Create a `.cursorrules` file in your project root:
```text .cursorrules theme={null}
# Email Testing with ZeroDrop
When writing E2E tests involving email flows, use zerodrop-client (TypeScript) or zerodrop (Python).
## TypeScript Setup
npm install zerodrop-client
## Python Setup
pip install zerodrop
## Rules
- Use generateInbox() / generate_inbox() for unique inbox per test — no network request needed
- Use email.otp / email.otp for OTP extraction (auto-extracted at edge, no regex)
- Use email.magicLink / email.magic_link for verification links (auto-extracted at edge, no regex)
- Never mock email in E2E tests
- Never share inboxes between tests
- Never use MailHog or fake SMTP in CI
## TypeScript Pattern (Playwright / Cypress)
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// email.otp — auto-extracted OTP code
// email.magicLink — auto-extracted verification link
## Python Pattern (pytest)
from zerodrop import ZeroDrop
mail = ZeroDrop()
inbox = mail.generate_inbox()
email = mail.wait_for_latest(inbox, timeout=30000)
# email.otp — auto-extracted OTP code
# email.magic_link — auto-extracted verification link
## Docs
https://docs.zerodrop.dev
```
## GitHub Copilot (.github/copilot-instructions.md)
Create `.github/copilot-instructions.md` in your repo:
```markdown .github/copilot-instructions.md theme={null}
## Email Testing
For E2E tests involving email flows (verification, OTP, password reset), use ZeroDrop.
### TypeScript (Playwright / Cypress)
Install: npm install zerodrop-client
Pattern:
- import { ZeroDrop } from 'zerodrop-client'
- const mail = new ZeroDrop()
- const inbox = process.env.TEST_INBOX ?? mail.generateInbox()
- const email = await mail.waitForLatest(inbox, { timeout: 30000 })
- Use email.otp for OTP codes (auto-extracted, no regex)
- Use email.magicLink for verification links (auto-extracted, no regex)
### Python (pytest)
Install: pip install zerodrop
Pattern:
- from zerodrop import ZeroDrop
- mail = ZeroDrop()
- inbox = mail.generate_inbox()
- email = mail.wait_for_latest(inbox, timeout=30000)
- Use email.otp for OTP codes (auto-extracted, no regex)
- Use email.magic_link for verification links (auto-extracted, no regex)
Never mock email in E2E tests. Never use MailHog or Mailpit in CI.
Docs: https://docs.zerodrop.dev
```
## Claude Code (CLAUDE.md)
Create a `CLAUDE.md` file in your project root:
```markdown CLAUDE.md theme={null}
## Email Testing
For E2E tests involving email flows (verification, OTP, password reset), use ZeroDrop.
### TypeScript (Playwright / Cypress)
Install: npm install zerodrop-client
Pattern:
- import { ZeroDrop } from 'zerodrop-client'
- const mail = new ZeroDrop()
- const inbox = process.env.TEST_INBOX ?? mail.generateInbox()
- const email = await mail.waitForLatest(inbox, { timeout: 30000 })
- Use email.otp for OTP codes (auto-extracted, no regex)
- Use email.magicLink for verification links (auto-extracted, no regex)
### Python (pytest)
Install: pip install zerodrop
Pattern:
- from zerodrop import ZeroDrop
- mail = ZeroDrop()
- inbox = mail.generate_inbox()
- email = mail.wait_for_latest(inbox, timeout=30000)
- Use email.otp for OTP codes (auto-extracted, no regex)
- Use email.magic_link for verification links (auto-extracted, no regex)
Never mock email in E2E tests. Never use MailHog or Mailpit in CI.
Docs: https://docs.zerodrop.dev
```
Claude Code reads `CLAUDE.md` automatically as project context on every prompt.
## What the AI will generate
### TypeScript (Playwright)
After adding the context file, asking your AI to write a Playwright test for an email verification flow will produce:
```typescript theme={null}
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
test('email verification flow', async ({ page }) => {
const inbox = mail.generateInbox();
await page.goto('/signup');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// Auto-extracted — no regex needed
await page.goto(email.magicLink!);
await expect(page).toHaveURL('/dashboard');
});
```
### Python (pytest)
Asking your AI to write a pytest test for an OTP verification flow will produce:
```python theme={null}
from zerodrop import ZeroDrop
mail = ZeroDrop()
def test_email_verification(page):
inbox = mail.generate_inbox()
page.goto('/signup')
page.fill('[data-testid="email"]', inbox)
page.click('[data-testid="submit"]')
email = mail.wait_for_latest(inbox, timeout=30000)
# Auto-extracted — no regex needed
assert email.otp is not None
page.fill('[data-testid="otp"]', email.otp)
page.click('[data-testid="verify"]')
assert page.url == '/dashboard'
```
No MailHog. No mocking. No regex.
# Fetch emails for an inbox
Source: https://docs.zerodrop.dev/api-reference/inbox/fetch-emails-for-an-inbox
/openapi.json get /api/inbox/{name}
Returns all emails currently stored for the given inbox name. Emails expire after 30 minutes. Returns an empty array if the inbox has no emails or has expired.
# Stream emails via SSE
Source: https://docs.zerodrop.dev/api-reference/inbox/stream-emails-via-sse
/openapi.json get /api/inbox/{name}/stream
Opens a Server-Sent Events stream for the given inbox. The server polls Redis with an exponential backoff strategy and pushes an event the moment an email arrives. The connection closes automatically after the first email or after 30 seconds (timeout).
**Backoff strategy:**
- 0–2s: polls every 500ms
- 2–10s: polls every 1000ms
- 10s+: polls every 2000ms
**Events:**
- `: connected` — connection established
- `: keepalive` — heartbeat every 10s to prevent proxy timeout
- `data: {email JSON}` — email arrived
- `data: __timeout__` — no email within 30s, connection closing
For Node.js environments, use `fetch()` with `ReadableStream` rather than `EventSource` (which is browser-only).
# Join the Workspaces waitlist
Source: https://docs.zerodrop.dev/api-reference/waitlist/join-the-workspaces-waitlist
/openapi.json post /api/waitlist
Adds an email address to the Workspaces waitlist. You will be contacted when Workspaces launches.
# Cypress
Source: https://docs.zerodrop.dev/cypress
# Cypress
ZeroDrop works with Cypress for end-to-end email testing. No Docker, no SMTP server, no mocking.
## Installation
```bash theme={null}
npm install zerodrop-client
npm install --save-dev cypress
```
## Basic setup
```javascript theme={null}
// cypress/e2e/auth.cy.js
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
describe('Email verification', () => {
it('user can sign up and verify email', () => {
const inbox = mail.generateInbox();
cy.visit('/signup');
cy.get('[data-testid="email"]').type(inbox);
cy.get('[data-testid="submit"]').click();
// Wait for email
cy.wrap(mail.waitForLatest(inbox, { timeout: 30000 }))
.then((email) => {
expect(email.otp).to.not.be.null;
// Enter OTP
cy.get('[data-testid="otp"]').type(email.otp);
cy.get('[data-testid="verify"]').click();
cy.url().should('include', '/dashboard');
});
});
});
```
## OTP verification
```javascript theme={null}
it('OTP login', () => {
const inbox = mail.generateInbox();
cy.visit('/login');
cy.get('[data-testid="email"]').type(inbox);
cy.get('[data-testid="submit"]').click();
cy.wrap(mail.waitForLatest(inbox, { timeout: 30000 }))
.then((email) => {
// OTP auto-extracted — no regex needed
cy.get('[data-testid="otp"]').type(email.otp);
cy.get('[data-testid="verify"]').click();
});
});
```
## Magic link verification
```javascript theme={null}
it('magic link verification', () => {
const inbox = mail.generateInbox();
cy.visit('/signup');
cy.get('[data-testid="email"]').type(inbox);
cy.get('[data-testid="submit"]').click();
cy.wrap(mail.waitForLatest(inbox, { timeout: 30000 }))
.then((email) => {
// Navigate to verification link
cy.visit(email.magicLink);
cy.url().should('include', '/dashboard');
});
});
```
## cypress.config.js
```javascript theme={null}
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
},
});
```
## In GitHub Actions
```yaml theme={null}
- name: Generate test inbox
id: inbox
uses: zerodrop-dev/setup-zerodrop@36c6685cce921fbe8b6353a71292c8e262124e3e # v1.0.0
- name: Run Cypress tests
run: npx cypress run
env:
TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
CYPRESS_BASE_URL: ${{ secrets.STAGING_URL }}
```
```javascript theme={null}
// Use CI inbox or generate locally
const inbox = Cypress.env('TEST_INBOX') ?? mail.generateInbox();
```
# GitHub Actions
Source: https://docs.zerodrop.dev/github-actions
# GitHub Actions
ZeroDrop has a native GitHub Action that generates a disposable inbox before your tests run and injects it as an environment variable.
## Basic usage
```yaml theme={null}
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate test inbox
id: inbox
uses: zerodrop-dev/setup-zerodrop@36c6685cce921fbe8b6353a71292c8e262124e3e # v1.0.0
- name: Run Playwright tests
run: npx playwright test
env:
TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
```
## Accessing the inbox in tests
```typescript theme={null}
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
// Uses CI inbox if available, generates one locally otherwise
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();
```
## SHA pinning (recommended)
Pin to a specific commit SHA for supply chain security:
```yaml theme={null}
# Recommended — pin to SHA
uses: zerodrop-dev/setup-zerodrop@36c6685cce921fbe8b6353a71292c8e262124e3e # v1.0.0
# Not recommended — floating tag
uses: zerodrop-dev/setup-zerodrop@v1
```
## Parallel matrix builds
Each matrix job gets its own isolated inbox automatically:
```yaml theme={null}
jobs:
test:
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- name: Generate test inbox
id: inbox
uses: zerodrop-dev/setup-zerodrop@36c6685cce921fbe8b6353a71292c8e262124e3e
- name: Run tests
run: npx playwright test --project=${{ matrix.browser }}
env:
TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
```
Each browser gets a different inbox — no collisions between parallel jobs.
## Full workflow example
```yaml theme={null}
name: E2E Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Generate test inbox
id: inbox
uses: zerodrop-dev/setup-zerodrop@36c6685cce921fbe8b6353a71292c8e262124e3e # v1.0.0
- name: Run E2E tests
run: npx playwright test
env:
TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
```
## Action outputs
| Output | Description | Example |
| ------- | ------------------ | ------------------------------------- |
| `inbox` | Full email address | `swift-x7k2m@zerodrop-sandbox.online` |
| `name` | Inbox name only | `swift-x7k2m` |
## Security
The Action generates inbox names locally on the runner — no network request is made during generation. The inbox address is a random string; it does not contact ZeroDrop servers until your tests begin polling.
See [SECURITY.md](https://github.com/zerodrop-dev/setup-zerodrop/blob/main/SECURITY.md) for full supply chain documentation.
# Go SDK
Source: https://docs.zerodrop.dev/go
Test email flows in Go — OTPs and magic links auto-extracted. Zero dependencies, stdlib only.
## Install
```bash theme={null}
go get github.com/zerodrop-dev/zerodrop-go
```
Zero dependencies — stdlib only. Go 1.21+.
## Quick start
```go theme={null}
package main
import (
"context"
"fmt"
"time"
zerodrop "github.com/zerodrop-dev/zerodrop-go"
)
func main() {
client := zerodrop.New()
inbox := client.GenerateInbox()
// => "swift-x7k29a@zerodrop-sandbox.online"
// Trigger your app's email flow with the inbox address...
email, err := client.WaitForLatest(context.Background(), inbox, &zerodrop.Options{
Timeout: 15 * time.Second,
})
if err != nil {
panic(err)
}
fmt.Println(email.OTP) // "123456" — auto-extracted, no regex
fmt.Println(email.MagicLink) // "https://..." — auto-extracted
}
```
## go test
```go theme={null}
func TestSignupEmailVerification(t *testing.T) {
client := zerodrop.New()
inbox := client.GenerateInbox()
// Trigger signup with the inbox address...
email, err := client.WaitForLatest(context.Background(), inbox, &zerodrop.Options{
Timeout: 15 * time.Second,
})
if err != nil {
t.Fatalf("no verification email: %v", err)
}
if email.OTP == "" {
t.Fatal("expected OTP in verification email")
}
}
```
## SSE — sub-second delivery
`WaitForLatest` uses Server-Sent Events by default with automatic polling fallback:
```go theme={null}
// Force polling mode if needed
email, err := client.WaitForLatest(ctx, inbox, &zerodrop.Options{
DisableSSE: true,
})
```
## Filtering
```go theme={null}
email, err := client.WaitForLatest(ctx, inbox, &zerodrop.Options{
Timeout: 15 * time.Second,
Filter: &zerodrop.Filter{
From: "noreply@yourapp.com",
Subject: "Verify",
HasOTP: zerodrop.Bool(true),
},
})
```
All string filters are case-insensitive partial matches.
## Parallel tests
`GenerateInbox` runs locally — safe with `t.Parallel()`, zero collisions:
```go theme={null}
func TestParallelSignups(t *testing.T) {
t.Parallel()
client := zerodrop.New()
inbox := client.GenerateInbox() // unique per call
// ...
}
```
## Error handling
```go theme={null}
var timeoutErr *zerodrop.TimeoutError
if errors.As(err, &timeoutErr) {
// No email arrived
}
if errors.Is(err, zerodrop.ErrUnauthorized) {
// Invalid API key
}
```
## Workspaces & self-hosting
```go theme={null}
client := zerodrop.New(
zerodrop.WithAPIKey(os.Getenv("ZERODROP_API_KEY")),
zerodrop.WithBaseURL("https://your-instance.yourdomain.com"),
)
```
## Links
* [pkg.go.dev reference](https://pkg.go.dev/github.com/zerodrop-dev/zerodrop-go)
* [GitHub](https://github.com/zerodrop-dev/zerodrop-go)
# Java SDK
Source: https://docs.zerodrop.dev/java
Test email flows in JUnit, Selenium and Playwright for Java — OTPs and magic links auto-extracted.
## Install
**Gradle:**
```groovy theme={null}
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
testImplementation 'com.github.zerodrop-dev:zerodrop-java:v0.1.0'
}
```
**Maven:**
```xml theme={null}
jitpack.io
https://jitpack.io
com.github.zerodrop-dev
zerodrop-java
v0.1.0
test
```
Java 11+ · one dependency (Gson).
## Quick start
```java theme={null}
import dev.zerodrop.ZeroDrop;
import dev.zerodrop.Email;
import java.time.Duration;
ZeroDrop mail = ZeroDrop.create();
String inbox = mail.generateInbox();
// => "swift-x7k29ab@zerodrop-sandbox.online"
// Trigger your app's email flow with the inbox address...
Email email = mail.waitForLatest(inbox, Duration.ofSeconds(15));
email.otp(); // Optional["123456"] — auto-extracted, no regex
email.magicLink(); // Optional["https://..."] — auto-extracted
```
## JUnit 5 + Selenium
```java theme={null}
@Test
void emailVerificationWithOtp() {
ZeroDrop mail = ZeroDrop.create();
String inbox = mail.generateInbox();
driver.get("https://yourapp.com/signup");
driver.findElement(By.name("email")).sendKeys(inbox);
driver.findElement(By.name("password")).sendKeys("TestPassword123!");
driver.findElement(By.cssSelector("button[type=submit]")).click();
Email email = mail.waitForLatest(inbox, Duration.ofSeconds(15));
String otp = email.otp().orElseThrow();
driver.findElement(By.name("otp")).sendKeys(otp);
driver.findElement(By.cssSelector("button[type=submit]")).click();
assertTrue(driver.getCurrentUrl().endsWith("/dashboard"));
}
```
## Filtering
```java theme={null}
import dev.zerodrop.Filter;
Filter filter = Filter.builder()
.from("noreply@yourapp.com")
.hasOtp(true)
.build();
Email email = mail.waitForLatest(inbox, Duration.ofSeconds(15), filter);
```
All string filters are case-insensitive partial matches.
## Parallel tests
`generateInbox()` runs locally — safe with JUnit parallel execution and TestNG parallel suites. Every call returns a unique isolated inbox.
## Error handling
```java theme={null}
try {
Email email = mail.waitForLatest(inbox, Duration.ofSeconds(15));
} catch (ZeroDropException.Timeout e) {
// No email arrived
} catch (ZeroDropException.Auth e) {
// Invalid API key
} catch (ZeroDropException.Network e) {
// Transport failure
}
```
## Workspaces & self-hosting
```java theme={null}
ZeroDrop mail = ZeroDrop.builder()
.apiKey(System.getenv("ZERODROP_API_KEY"))
.baseUrl("https://your-instance.yourdomain.com")
.build();
```
## Links
* [JitPack](https://jitpack.io/#zerodrop-dev/zerodrop-java)
* [GitHub](https://github.com/zerodrop-dev/zerodrop-java)
# PHP SDK
Source: https://docs.zerodrop.dev/php
Test email flows in PHPUnit, Pest and Laravel Dusk — OTPs and magic links auto-extracted.
## Install
```bash theme={null}
composer require --dev zerodrop/zerodrop
```
PHP 8.1+ · ext-curl · ext-json — no other dependencies.
## Quick start
```php theme={null}
use ZeroDrop\Client;
$mail = new Client();
$inbox = $mail->generateInbox();
// => "swift-x7k29ab@zerodrop-sandbox.online"
// Trigger your app's email flow with the inbox address...
$email = $mail->waitForLatest($inbox, timeout: 15);
$email->otp; // "123456" — auto-extracted, no regex
$email->magicLink; // "https://..." — auto-extracted
```
## Laravel Dusk
```php theme={null}
use Laravel\Dusk\Browser;
use ZeroDrop\Client;
public function testSignupEmailVerification(): void
{
$mail = new Client();
$inbox = $mail->generateInbox();
$this->browse(function (Browser $browser) use ($mail, $inbox) {
$browser->visit('/signup')
->type('email', $inbox)
->type('password', 'TestPassword123!')
->press('Create account');
$email = $mail->waitForLatest($inbox, timeout: 15);
$browser->type('otp', $email->otp)
->press('Verify')
->assertPathIs('/dashboard');
});
}
```
## Pest
```php theme={null}
use ZeroDrop\Client;
it('verifies the account via emailed OTP', function () {
$mail = new Client();
$inbox = $mail->generateInbox();
// Trigger signup with $inbox...
$email = $mail->waitForLatest($inbox, timeout: 15);
expect($email->otp)->not->toBeNull();
});
```
## Filtering
```php theme={null}
use ZeroDrop\Filter;
$email = $mail->waitForLatest(
$inbox,
timeout: 15,
filter: new Filter(
from: 'noreply@yourapp.com',
hasOtp: true,
),
);
```
All string filters are case-insensitive partial matches.
## Parallel tests
`generateInbox()` runs locally — safe with Paratest and parallel Dusk runs. Every call returns a unique isolated inbox.
## Error handling
```php theme={null}
use ZeroDrop\Exception\TimeoutException;
use ZeroDrop\Exception\AuthException;
use ZeroDrop\Exception\NetworkException;
try {
$email = $mail->waitForLatest($inbox, timeout: 15);
} catch (TimeoutException $e) {
// No email arrived
} catch (AuthException $e) {
// Invalid API key
} catch (NetworkException $e) {
// Transport failure
}
```
## Workspaces & self-hosting
```php theme={null}
$mail = new Client(
apiKey: getenv('ZERODROP_API_KEY'),
baseUrl: 'https://your-instance.yourdomain.com',
);
```
## Links
* [Packagist](https://packagist.org/packages/zerodrop/zerodrop)
* [GitHub](https://github.com/zerodrop-dev/zerodrop-php)
# Playwright
Source: https://docs.zerodrop.dev/playwright
# Playwright
ZeroDrop integrates with Playwright for end-to-end email testing in CI pipelines. No Docker, no SMTP server, no mocking.
## Installation
```bash theme={null}
npm install zerodrop-client
npm install --save-dev @playwright/test
```
## Basic setup
```typescript theme={null}
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
test('email verification flow', async ({ page }) => {
// Generate a unique inbox for this test
const inbox = mail.generateInbox();
// Use the inbox in your signup form
await page.goto('/signup');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
// Wait for the verification email
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// OTP and magic link are auto-extracted
console.log(email.otp); // "123456"
console.log(email.magicLink); // "https://yourapp.com/verify?token=..."
// Click the verification link
await page.goto(email.magicLink!);
await expect(page).toHaveURL('/dashboard');
});
```
## OTP verification
ZeroDrop extracts OTPs automatically at the edge — no regex needed in your tests.
```typescript theme={null}
test('OTP login flow', async ({ page }) => {
const inbox = mail.generateInbox();
await page.goto('/login');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
// Wait for OTP email
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
expect(email.otp).not.toBeNull();
// Enter OTP directly — no body parsing needed
await page.fill('[data-testid="otp"]', email.otp!);
await page.click('[data-testid="verify"]');
await expect(page).toHaveURL('/dashboard');
});
```
## Password reset flow
```typescript theme={null}
test('password reset flow', async ({ page }) => {
const inbox = mail.generateInbox();
// Request password reset
await page.goto('/forgot-password');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
// Wait for reset email
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// Navigate to reset link
await page.goto(email.magicLink!);
await page.fill('[data-testid="password"]', 'NewPassword123!');
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/login');
});
```
## Parallel tests
Every inbox is isolated — parallel tests never collide.
```typescript theme={null}
test.describe.parallel('Email flows', () => {
test('user A can verify email', async ({ page }) => {
const inbox = mail.generateInbox(); // unique per test
// ...
});
test('user B can verify email', async ({ page }) => {
const inbox = mail.generateInbox(); // different inbox
// ...
});
});
```
## Multiple emails in one test
```typescript theme={null}
test('signup then password reset', async ({ page }) => {
const inbox = mail.generateInbox();
let lastEmailId: string;
// First email — verification
const verifyEmail = await mail.waitForLatest(inbox, { timeout: 30000 });
lastEmailId = verifyEmail.id;
await page.goto(verifyEmail.magicLink!);
// Trigger password reset
await page.goto('/forgot-password');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
// Poll until a NEW email arrives
let resetEmail = null;
const deadline = Date.now() + 30000;
while (Date.now() < deadline) {
const latest = await mail.fetchLatest(inbox);
if (latest && latest.id !== lastEmailId) {
resetEmail = latest;
break;
}
await new Promise(r => setTimeout(r, 2000));
}
expect(resetEmail).not.toBeNull();
await page.goto(resetEmail!.magicLink!);
});
```
## Timeout options
```typescript theme={null}
// Default: 10 seconds
const email = await mail.waitForLatest(inbox);
// Custom timeout
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// Force polling mode (disable SSE)
const email = await mail.waitForLatest(inbox, { sse: false });
```
## playwright.config.ts
```typescript theme={null}
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 60000, // allow time for email delivery
use: {
baseURL: 'http://localhost:3000',
},
});
```
# Python SDK
Source: https://docs.zerodrop.dev/python
ZeroDrop Python SDK reference — email.otp and email.magic_link auto-extracted. No Docker, no regex, no signup.
# Python SDK
Full API reference for `zerodrop` — the ZeroDrop Python SDK.
## Installation
```bash theme={null}
pip install zerodrop
```
No dependencies. Python 3.8+.
## ZeroDrop class
```python theme={null}
from zerodrop import ZeroDrop
mail = ZeroDrop(api_key=None, base_url="https://zerodrop.dev")
```
### Parameters
| Parameter | Type | Description |
| ---------- | ---------------- | ----------------------------------------- |
| `api_key` | `str` (optional) | API key for Workspaces tier |
| `base_url` | `str` (optional) | Custom base URL for self-hosted instances |
Without an API key, ZeroDrop runs in free sandbox mode with a shared domain and 30-minute TTL.
***
## generate\_inbox()
```python theme={null}
mail.generate_inbox() -> str
```
Generates a unique inbox address instantly. No network request.
```python theme={null}
inbox = mail.generate_inbox()
# "swift-x7k2m@zerodrop-sandbox.online"
```
***
## wait\_for\_latest()
```python theme={null}
mail.wait_for_latest(
inbox: str,
timeout: int = 10000,
poll_interval: float = 2.0,
filter_: Optional[ZeroDropFilter] = None
) -> ZeroDropEmail
```
Polls until an email arrives in the inbox. Raises `ZeroDropTimeoutError` if no email arrives within the timeout.
### Parameters
| Parameter | Type | Default | Description |
| --------------- | ---------------- | -------- | ---------------------------------------- |
| `inbox` | `str` | required | Inbox address to poll |
| `timeout` | `int` | `10000` | Milliseconds to wait |
| `poll_interval` | `float` | `2.0` | Seconds between polls |
| `filter_` | `ZeroDropFilter` | `None` | Optional filter to match specific emails |
```python theme={null}
# Default — 10s timeout
email = mail.wait_for_latest(inbox)
# Custom timeout
email = mail.wait_for_latest(inbox, timeout=30000)
# With filter
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(has_otp=True))
```
***
## fetch\_latest()
```python theme={null}
mail.fetch_latest(
inbox: str,
filter_: Optional[ZeroDropFilter] = None
) -> Optional[ZeroDropEmail]
```
Fetches the latest email without waiting. Returns `None` if the inbox is empty or no email matches the filter.
```python theme={null}
email = mail.fetch_latest(inbox)
if email:
print(email.subject)
```
***
## Email Filtering
Filter emails by sender, subject, body, or extracted fields. Useful when multiple emails land in the same inbox.
```python theme={null}
from zerodrop import ZeroDrop, ZeroDropFilter
mail = ZeroDrop()
# Only match emails from a specific sender
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
from_="noreply@yourapp.com"
))
# Only match emails with a specific subject
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
subject="Verify your email"
))
# Only match emails that contain an OTP
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
has_otp=True
))
# Only match emails that contain a magic link
email = mail.wait_for_latest(inbox, filter_=ZeroDropFilter(
has_magic_link=True
))
# Combine multiple filters
email = mail.wait_for_latest(inbox, timeout=15000, filter_=ZeroDropFilter(
from_="noreply@yourapp.com",
subject="Reset",
has_magic_link=True
))
```
All string filters are case-insensitive partial matches.
***
## ZeroDropEmail type
```python theme={null}
@dataclass
class ZeroDropEmail:
id: str # Unique message ID
from_: str # Sender address
to: str # Recipient address
subject: str # Email subject
body: str # Parsed plain-text body
raw_body: str # Full raw MIME message
received_at: datetime # UTC timestamp
otp: Optional[str] # Auto-extracted OTP code (4-8 digits)
magic_link: Optional[str] # Auto-extracted verification/reset link
```
### otp
4-8 digit numeric code extracted from the email body. Detected near labels like `code`, `otp`, `pin`, `verification`. `None` if not detected.
```python theme={null}
email = mail.wait_for_latest(inbox)
print(email.otp) # "123456" or None
```
### magic\_link
Verification or reset URL extracted from the email body. Detected for URLs containing `verify`, `confirm`, `reset`, `token`, `activate`, or `auth`. `None` if not detected.
```python theme={null}
print(email.magic_link) # "https://app.com/verify?token=abc" or None
```
***
## ZeroDropFilter type
```python theme={null}
@dataclass
class ZeroDropFilter:
from_: Optional[str] = None # Partial match on sender address
subject: Optional[str] = None # Partial match on subject line
body: Optional[str] = None # Partial match on email body
has_otp: Optional[bool] = None # Only match emails with an OTP
has_magic_link: Optional[bool] = None # Only match emails with a magic link
```
***
## Error types
### ZeroDropTimeoutError
Raised when no email arrives within the timeout.
```python theme={null}
from zerodrop import ZeroDrop, ZeroDropTimeoutError
try:
email = mail.wait_for_latest(inbox, timeout=10000)
except ZeroDropTimeoutError:
print("No email received in time")
```
### ZeroDropNetworkError
Raised when the API is unreachable. Includes a link to the status page.
```python theme={null}
from zerodrop import ZeroDropNetworkError
try:
email = mail.fetch_latest(inbox)
except ZeroDropNetworkError as e:
print(f"Network error — check https://zerodrop.instatus.com")
```
### ZeroDropAuthError
Raised when an invalid API key is provided.
***
## pytest example
```python theme={null}
import pytest
from zerodrop import ZeroDrop
mail = ZeroDrop()
def test_signup_email_verification(page):
inbox = mail.generate_inbox()
page.goto("/signup")
page.fill('[name="email"]', inbox)
page.fill('[name="password"]', "TestPassword123!")
page.click('[type="submit"]')
# Wait for verification email
email = mail.wait_for_latest(inbox, timeout=15000)
# OTP auto-extracted — no regex needed
assert email.otp is not None
page.fill('[name="otp"]', email.otp)
page.click('[type="submit"]')
assert page.url == "/dashboard"
def test_magic_link_login(page):
inbox = mail.generate_inbox()
page.goto("/login")
page.fill('[name="email"]', inbox)
page.click('button:has-text("Send magic link")')
email = mail.wait_for_latest(inbox, timeout=15000)
# magic_link auto-extracted — no HTML parsing needed
assert email.magic_link is not None
page.goto(email.magic_link)
assert page.url == "/dashboard"
```
***
## Parallel test runs
`generate_inbox()` runs locally — no network request, no throttling:
```python theme={null}
# Safe to run in parallel — generate_inbox() is local
inboxes = [mail.generate_inbox() for _ in range(50)]
```
***
## Free vs Workspace
| | Free | Workspace |
| --------------------- | ------ | --------- |
| Inbox generation | ✓ | ✓ |
| OTP auto-extraction | ✓ | ✓ |
| Magic link extraction | ✓ | ✓ |
| Email filtering | ✓ | ✓ |
| Email retention | 30 min | Extended |
| Custom domains | ✗ | ✓ |
| API key | ✗ | ✓ |
| Webhooks | ✗ | ✓ |
| AI spam filter | On | Off |
Get a Workspace at [zerodrop.dev/pricing](https://zerodrop.dev/pricing)
***
## Zero telemetry
The SDK does not send analytics, usage metrics, or environment data to any server. The only network requests made are explicit inbox polls to `zerodrop.dev/api/inbox/{name}`.
Your CI pipeline is your business.
# Quickstart
Source: https://docs.zerodrop.dev/quickstart
# Quick Start
ZeroDrop gives you a disposable email inbox in one line of code. No signup, no Docker, no SMTP config.
## Install the SDK
```bash theme={null}
npm install zerodrop-client
```
## Generate an inbox
```typescript theme={null}
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
const inbox = mail.generateInbox();
console.log(inbox); // swift-x7k2m@zerodrop-sandbox.online
```
The inbox is ready instantly — no network request needed to generate it.
## Wait for an email
```typescript theme={null}
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
console.log(email.subject); // "Verify your email"
console.log(email.otp); // "123456" — auto-extracted
console.log(email.magicLink); // "https://..." — auto-extracted
```
`waitForLatest` uses SSE for sub-second delivery. Falls back to polling automatically if SSE is unavailable.
## Full example
```typescript theme={null}
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
test('user can sign up and verify email', async ({ page }) => {
const inbox = mail.generateInbox();
await page.goto('/signup');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// OTP already extracted — no regex needed
await page.fill('[data-testid="otp"]', email.otp!);
await page.click('[data-testid="verify"]');
await expect(page).toHaveURL('/dashboard');
});
```
## Next steps
* [Playwright guide](/playwright) — full email verification test examples
* [GitHub Actions](/github-actions) — use ZeroDrop in CI pipelines
* [SDK reference](/sdk-reference) — full API documentation
* [Self-hosting](/self-hosting) — deploy your own instance
# Ruby SDK
Source: https://docs.zerodrop.dev/ruby
Test email flows in RSpec, Minitest and Capybara — OTPs and magic links auto-extracted. Zero dependencies.
## Install
```bash theme={null}
gem install zerodrop
```
Or in your Gemfile:
```ruby theme={null}
gem "zerodrop", group: :test
```
Zero runtime dependencies. Ruby 3.0+.
## Quick start
```ruby theme={null}
require "zerodrop"
mail = ZeroDrop::Client.new
inbox = mail.generate_inbox
# => "swift-x7k29ab@zerodrop-sandbox.online"
# Trigger your app's email flow with the inbox address...
email = mail.wait_for_latest(inbox, timeout: 15)
email.otp # => "123456" — auto-extracted, no regex
email.magic_link # => "https://..." — auto-extracted
```
## RSpec + Capybara
```ruby theme={null}
RSpec.describe "Signup email verification", type: :system do
let(:mail) { ZeroDrop::Client.new }
it "verifies the account via emailed OTP" do
inbox = mail.generate_inbox
visit "/signup"
fill_in "Email", with: inbox
fill_in "Password", with: "TestPassword123!"
click_button "Create account"
email = mail.wait_for_latest(inbox, timeout: 15)
expect(email.otp).not_to be_nil
fill_in "Code", with: email.otp
click_button "Verify"
expect(page).to have_current_path("/dashboard")
end
end
```
## Filtering
```ruby theme={null}
email = mail.wait_for_latest(
inbox,
timeout: 15,
filter: ZeroDrop::Filter.new(
from: "noreply@yourapp.com",
has_otp: true
)
)
```
All string filters are case-insensitive partial matches.
## Parallel tests
`generate_inbox` runs locally — safe with parallel\_tests, flatware, turbo\_tests. Every call returns a unique isolated inbox.
## Error handling
```ruby theme={null}
begin
email = mail.wait_for_latest(inbox, timeout: 15)
rescue ZeroDrop::TimeoutError
# No email arrived
rescue ZeroDrop::AuthError
# Invalid API key
rescue ZeroDrop::NetworkError => e
# Transport failure
end
```
## Workspaces & self-hosting
```ruby theme={null}
mail = ZeroDrop::Client.new(
api_key: ENV["ZERODROP_API_KEY"],
base_url: "https://your-instance.yourdomain.com"
)
```
## Links
* [RubyGems](https://rubygems.org/gems/zerodrop)
* [GitHub](https://github.com/zerodrop-dev/zerodrop-ruby)
# SDK Reference
Source: https://docs.zerodrop.dev/sdk-reference
# SDK Reference
Full API reference for `zerodrop-client`.
## Installation
```bash theme={null}
npm install zerodrop-client
```
## ZeroDrop class
```typescript theme={null}
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop(apiKey?, options?);
```
### Parameters
| Parameter | Type | Description |
| ----------------- | ------------------- | ----------------------------------------- |
| `apiKey` | `string` (optional) | API key for Workspaces tier |
| `options.baseUrl` | `string` (optional) | Custom base URL for self-hosted instances |
Without an API key, ZeroDrop runs in free sandbox mode with a shared domain and 30-minute TTL.
***
## generateInbox()
```typescript theme={null}
mail.generateInbox(): string
```
Generates a unique inbox address instantly. No network request.
```typescript theme={null}
const inbox = mail.generateInbox();
// "swift-x7k2m@zerodrop-sandbox.online"
```
***
## waitForLatest()
```typescript theme={null}
mail.waitForLatest(inbox: string, options?): Promise
```
Waits for an email to arrive. Uses SSE for sub-second delivery, falls back to polling automatically.
Throws `ZeroDropTimeoutError` if no email arrives within the timeout.
### Options
| Option | Type | Default | Description |
| -------------- | --------- | ------- | ------------------------------------------ |
| `timeout` | `number` | `10000` | Milliseconds to wait |
| `pollInterval` | `number` | `2000` | Milliseconds between polls (fallback mode) |
| `sse` | `boolean` | `true` | Use SSE for delivery |
```typescript theme={null}
// Default — SSE, 10s timeout
const email = await mail.waitForLatest(inbox);
// Custom timeout
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
// Force polling
const email = await mail.waitForLatest(inbox, { sse: false });
```
***
## fetchLatest()
```typescript theme={null}
mail.fetchLatest(inbox: string): Promise
```
Fetches the latest email without waiting. Returns `null` if the inbox is empty.
```typescript theme={null}
const email = await mail.fetchLatest(inbox);
if (email) {
console.log(email.subject);
}
```
***
## ZeroDropEmail type
```typescript theme={null}
interface ZeroDropEmail {
id: string; // Unique message ID
from: string; // Sender address
to: string; // Recipient address
subject: string; // Email subject
body: string; // Parsed plain-text body
rawBody: string; // Full raw MIME message
receivedAt: Date; // UTC timestamp
otp: string | null; // Auto-extracted OTP code (4-8 digits)
magicLink: string | null; // Auto-extracted verification/reset link
}
```
### otp
4-8 digit numeric code extracted from the email body. Detected near labels like `code`, `otp`, `pin`, `verification`. `null` if not detected.
```typescript theme={null}
const email = await mail.waitForLatest(inbox);
console.log(email.otp); // "123456" or null
```
### magicLink
Verification or reset URL extracted from the email body. Detected for URLs containing `verify`, `confirm`, `reset`, `token`, `activate`, or `auth`. `null` if not detected.
```typescript theme={null}
console.log(email.magicLink); // "https://app.com/verify?token=abc" or null
```
***
## Error types
### ZeroDropTimeoutError
Thrown when no email arrives within the timeout.
```typescript theme={null}
import { ZeroDropTimeoutError } from 'zerodrop-client';
try {
const email = await mail.waitForLatest(inbox, { timeout: 10000 });
} catch (err) {
if (err instanceof ZeroDropTimeoutError) {
console.log('No email received in time');
}
}
```
### ZeroDropNetworkError
Thrown when the API is unreachable. Includes a link to the status page.
```typescript theme={null}
import { ZeroDropNetworkError } from 'zerodrop-client';
try {
const email = await mail.fetchLatest(inbox);
} catch (err) {
if (err instanceof ZeroDropNetworkError) {
console.log('Network error — check https://zerodrop.instatus.com');
}
}
```
### ZeroDropAuthError
Thrown when an invalid API key is provided.
***
## Zero telemetry
The SDK does not send analytics, usage metrics, or environment data to any server. The only network requests made are explicit inbox polls to `zerodrop.dev/api/inbox/{name}`.
Your CI pipeline is your business.
# Self-Hosting
Source: https://docs.zerodrop.dev/self-hosting
# Self-Hosting
ZeroDrop's email worker is fully open source. Teams with strict compliance requirements can deploy their own instance.
## What you need
* A Cloudflare account with Workers enabled
* A domain added to Cloudflare (for Email Routing)
* An Upstash Redis database
* Node.js 18+ and Wrangler CLI
## Setup
```bash theme={null}
git clone https://github.com/zerodrop-dev/zerodrop-worker.git
cd zerodrop-worker
npm install
```
Set your secrets:
```bash theme={null}
npx wrangler secret put UPSTASH_REDIS_REST_URL
npx wrangler secret put UPSTASH_REDIS_REST_TOKEN
```
Deploy:
```bash theme={null}
npx wrangler deploy
```
Configure Cloudflare Email Routing to send all emails at `*@yourdomain.com` to your deployed worker.
## Point the SDK at your instance
```typescript theme={null}
const mail = new ZeroDrop(undefined, {
baseUrl: 'https://your-dashboard.yourdomain.com'
});
```
## Full guide
See [SELF\_HOSTING.md](https://github.com/zerodrop-dev/zerodrop-worker/blob/master/SELF_HOSTING.md) in the worker repo for the complete setup guide including wrangler.toml configuration, disabling the AI spam filter, and cost estimates.
## Cost estimate
* **Cloudflare Workers** — free tier: 100,000 requests/day
* **Cloudflare Email Routing** — free
* **Cloudflare Workers AI** — free tier: 10,000 neurons/day
* **Upstash Redis** — free tier: 10,000 commands/day
For most CI pipelines, self-hosting runs entirely within free tiers.