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):

FieldTypeDefaultDescription
ttlnumber3600TTL in seconds.
labelstringundefinedOptional 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):

FieldTypeDefaultDescription
timeoutnumber60000Max wait time in milliseconds.
pollIntervalnumber2000Initial 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):

FieldTypeDefaultDescription
tostringDestination address.
subjectstringEmail subject.
textBodystringundefinedPlain text body.
htmlBodystringundefinedHTML body.
attachmentsarrayundefinedAttachment objects.
ttlnumber300TTL in seconds.
labelstringundefinedOptional 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

ExceptionHTTP StatusWhen
UnauthorizedError401Invalid or missing API key.
QuotaExceededError402Quota and credits exhausted.
NotFoundError404Address not found / no email.
ExpiredError410Address expired.
RateLimitedError429Rate limit exceeded.
OneShotErrorAnyBase 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