JavaScript / TypeScript SDK
Complete guide to the OneShotMail JS/TS SDK -- installation, API reference, Playwright, Cypress, and Jest integration.
Installation
npm install oneshot-mail
# or
yarn add oneshot-mail
# or
pnpm add oneshot-mail
Works in Node.js 18+ (uses the built-in fetch API). Full TypeScript support with exported types.
Configuration
From environment variable
import { OneShotClient } from "oneshot-mail";
// Reads ONESHOT_API_KEY from process.env automatically
const client = new OneShotClient();
Explicit API key
const client = new OneShotClient("osm_live_your_key");
Custom base URL
const client = new OneShotClient("osm_live_your_key", "http://localhost:4566/v1");
Module-level convenience functions
import { create, waitForEmail, send, deleteByLabel } from "oneshot-mail";
// These use a default client initialized from ONESHOT_API_KEY
const addr = await create({ label: "test" });
const email = await waitForEmail(addr.id);
API Reference
All methods are async and return Promises.
create(options?): Promise<Address>
Create a new one-shot email address.
Options (CreateOptions):
| Field | Type | Default | Description |
|---|---|---|---|
ttl | number | 3600 | TTL in seconds. |
label | string | undefined | Optional label. |
mode | "receive" | "send" | "receive" | Address mode. |
const addr = await client.create({ ttl: 300, label: "signup-test" });
console.log(addr.id); // "abc123xyz789def456"
console.log(addr.address); // "abc123xyz789def456@in.oneshotemail.com"
console.log(addr.status); // "waiting"
get(addressId): Promise<Address>
const addr = await client.get("abc123xyz789def456");
if (addr.email) {
console.log(`Subject: ${addr.email.subject}`);
}
getEmail(addressId): Promise<Email>
const email = await client.getEmail("abc123xyz789def456");
console.log(email.from); // "noreply@example.com"
console.log(email.subject); // "Verify your account"
console.log(email.text_body); // "Click here..."
console.log(email.html_body); // "<html>...</html>"
console.log(email.attachments); // [{ filename, content_type, size_bytes, download_url }]
getEmailRaw(addressId): Promise<string>
const raw = await client.getEmailRaw("abc123xyz789def456");
// Full RFC 822 email source
downloadAttachment(addressId, index): Promise<ArrayBuffer>
const data = await client.downloadAttachment("abc123xyz789def456", 0);
const buffer = Buffer.from(data);
fs.writeFileSync("invoice.pdf", buffer);
waitForEmail(addressId, options?): Promise<Email>
Poll until an email arrives. This is the primary method for test suites.
Options (WaitOptions):
| Field | Type | Default | Description |
|---|---|---|---|
timeout | number | 60000 | Max wait time in milliseconds. |
pollInterval | number | 2000 | Initial polling interval in milliseconds. |
Note: timeout and pollInterval are in milliseconds (not seconds), consistent with JavaScript conventions.
const email = await client.waitForEmail(addr.id, { timeout: 30000 });
console.log(email.subject);
Throws: OneShotError with code "TIMEOUT" if no email arrives. ExpiredError if the address expires.
send(options): Promise<Address>
Options (SendOptions):
| Field | Type | Default | Description |
|---|---|---|---|
to | string | Destination address. | |
subject | string | Email subject. | |
textBody | string | undefined | Plain text body. |
htmlBody | string | undefined | HTML body. |
attachments | array | undefined | Attachment objects. |
ttl | number | 300 | TTL in seconds. |
label | string | undefined | Optional label. |
const result = await client.send({
to: "intake@myapp.com",
subject: "Test invoice",
textBody: "Please process this invoice.",
attachments: [
{
filename: "invoice.pdf",
content_type: "application/pdf",
content_base64: Buffer.from(pdfBytes).toString("base64"),
},
],
});
list(options?): Promise<Address[]>
const addresses = await client.list({ status: "waiting", label: "ci-run", limit: 10 });
addresses.forEach((addr) => console.log(`${addr.id}: ${addr.status}`));
delete(addressId): Promise<void>
await client.delete("abc123xyz789def456");
deleteByLabel(label): Promise<void>
await client.deleteByLabel("ci-run-abc123");
account(): Promise<Account>
const acct = await client.account();
console.log(`Plan: ${acct.plan}, Credits: ${acct.credits_remaining}`);
health(): Promise<HealthStatus>
const h = await client.health();
console.log(`Status: ${h.status}, Region: ${h.region}`);
TypeScript types
The SDK exports all types:
import type {
Address,
Email,
EmailSummary,
Attachment,
Account,
HealthStatus,
CreateOptions,
SendOptions,
WaitOptions,
ListOptions,
} from "oneshot-mail";
Error handling
| Exception | HTTP Status | When |
|---|---|---|
UnauthorizedError | 401 | Invalid or missing API key. |
QuotaExceededError | 402 | Quota and credits exhausted. |
NotFoundError | 404 | Address not found / no email. |
ExpiredError | 410 | Address expired. |
RateLimitedError | 429 | Rate limit exceeded. |
OneShotError | Any | Base class for all errors. |
All error classes have: code (string), message (string), statusCode (number). QuotaExceededError also has upgradeUrl.
import { QuotaExceededError, OneShotError } from "oneshot-mail";
try {
const email = await client.waitForEmail(addr.id, { timeout: 30000 });
} catch (err) {
if (err instanceof QuotaExceededError) {
console.error(`Upgrade at: ${err.upgradeUrl}`);
} else if (err instanceof OneShotError && err.code === "TIMEOUT") {
console.error("Email did not arrive in time");
} else {
throw err;
}
}
Playwright integration
Playwright is the most popular browser automation framework for Node.js. Here is a complete integration for testing email-dependent flows.
package.json
{
"devDependencies": {
"@playwright/test": "^1.40.0",
"oneshot-mail": "^0.1.0"
}
}
playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 60000,
use: {
baseURL: "http://localhost:3000",
},
});
tests/signup.spec.ts
import { test, expect } from "@playwright/test";
import { OneShotClient } from "oneshot-mail";
const oneshot = new OneShotClient(process.env.ONESHOT_API_KEY);
test.describe("Signup flow", () => {
let addressId: string;
let emailAddr: string;
test.afterEach(async () => {
if (addressId) {
await oneshot.delete(addressId).catch(() => {});
}
});
test("sends verification email and user can verify", async ({ page }) => {
// 1. Create a temporary email address
const addr = await oneshot.create({ ttl: 300, label: "playwright-signup" });
addressId = addr.id;
emailAddr = addr.address;
// 2. Fill out the signup form in the browser
await page.goto("/signup");
await page.fill('[name="email"]', emailAddr);
await page.fill('[name="password"]', "SecureP@ss1");
await page.fill('[name="name"]', "Test User");
await page.click('button[type="submit"]');
// 3. Verify the success page
await expect(page.locator(".success-message")).toContainText(
"Check your email"
);
// 4. Wait for the verification email
const email = await oneshot.waitForEmail(addr.id, { timeout: 30000 });
expect(email.subject).toContain("Verify your account");
// 5. Extract the verification link and visit it
const linkMatch = email.html_body.match(/href="(https?:\/\/[^"]*verify[^"]*)"/);
expect(linkMatch).toBeTruthy();
const verifyUrl = linkMatch![1];
await page.goto(verifyUrl);
await expect(page.locator("h1")).toContainText("Email Verified");
});
});
Running
npx playwright install
ONESHOT_API_KEY=osm_live_your_key npx playwright test
Cypress integration
Cypress runs in the browser, so you need to use cy.task() to call the OneShotMail SDK from Node.js.
cypress.config.ts
import { defineConfig } from "cypress";
import { OneShotClient } from "oneshot-mail";
const oneshot = new OneShotClient(process.env.ONESHOT_API_KEY);
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
setupNodeEvents(on, config) {
on("task", {
async createEmailAddress(options: { label?: string } = {}) {
const addr = await oneshot.create({
ttl: 300,
label: options.label || "cypress",
});
return { id: addr.id, address: addr.address };
},
async waitForEmail({ addressId, timeout = 30000 }) {
const email = await oneshot.waitForEmail(addressId, { timeout });
return {
from: email.from,
subject: email.subject,
text_body: email.text_body,
html_body: email.html_body,
};
},
async deleteAddress(addressId: string) {
await oneshot.delete(addressId);
return null;
},
async deleteByLabel(label: string) {
await oneshot.deleteByLabel(label);
return null;
},
});
},
},
});
Custom commands: cypress/support/commands.ts
Cypress.Commands.add("createEmailAddress", (label?: string) => {
return cy.task("createEmailAddress", { label });
});
Cypress.Commands.add("waitForEmail", (addressId: string, timeout?: number) => {
return cy.task("waitForEmail", { addressId, timeout });
});
Cypress.Commands.add("deleteAddress", (addressId: string) => {
return cy.task("deleteAddress", addressId);
});
declare global {
namespace Cypress {
interface Chainable {
createEmailAddress(label?: string): Chainable<{ id: string; address: string }>;
waitForEmail(addressId: string, timeout?: number): Chainable<{
from: string;
subject: string;
text_body: string;
html_body: string;
}>;
deleteAddress(addressId: string): Chainable<void>;
}
}
}
Test: cypress/e2e/signup.cy.ts
describe("Signup flow", () => {
let addressId: string;
afterEach(() => {
if (addressId) {
cy.deleteAddress(addressId);
}
});
it("sends a verification email", () => {
cy.createEmailAddress("cypress-signup").then((addr) => {
addressId = addr.id;
cy.visit("/signup");
cy.get('[name="email"]').type(addr.address);
cy.get('[name="password"]').type("SecureP@ss1");
cy.get('button[type="submit"]').click();
cy.contains("Check your email").should("be.visible");
cy.waitForEmail(addr.id, 30000).then((email) => {
expect(email.subject).to.contain("Verify your account");
});
});
});
});
Jest integration
jest.config.ts
export default {
preset: "ts-jest",
testTimeout: 60000,
};
Test: tests/email.test.ts
import { OneShotClient } from "oneshot-mail";
const oneshot = new OneShotClient(process.env.ONESHOT_API_KEY);
describe("Email notifications", () => {
const addressIds: string[] = [];
afterAll(async () => {
// Clean up all addresses
for (const id of addressIds) {
await oneshot.delete(id).catch(() => {});
}
});
it("sends a welcome email on signup", async () => {
const addr = await oneshot.create({ ttl: 300, label: "jest-signup" });
addressIds.push(addr.id);
// Trigger your app's signup API
await fetch("http://localhost:3000/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: addr.address, name: "Test User" }),
});
const email = await oneshot.waitForEmail(addr.id, { timeout: 30000 });
expect(email.subject).toContain("Welcome");
expect(email.text_body).toContain("Test User");
});
it("sends a password reset email", async () => {
const addr = await oneshot.create({ ttl: 300, label: "jest-reset" });
addressIds.push(addr.id);
await fetch("http://localhost:3000/api/password-reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: addr.address }),
});
const email = await oneshot.waitForEmail(addr.id, { timeout: 30000 });
expect(email.subject).toContain("Reset your password");
// Extract the reset link
const match = email.text_body.match(/https?:\/\/\S+reset\S*/);
expect(match).toBeTruthy();
});
});
Running
ONESHOT_API_KEY=osm_live_your_key npx jest