npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

shipmail

v0.1.23

Published

Official TypeScript SDK for the ShipMail API

Downloads

927

Readme

Shipmail TypeScript SDK

npm version npm downloads bundle size types license

Official TypeScript SDK for the Shipmail API. Zero runtime dependencies. Native fetch. Full TypeScript types. ESM and CommonJS.

Runtimes: Node.js 18+, Bun, Deno. Webhook verification uses node:crypto (enable nodejs_compat on Cloudflare Workers if you verify webhooks there).

Contents

Install

bun add shipmail
# or
npm install shipmail
# or
pnpm add shipmail

Quick start

import { ShipMailClient } from "shipmail";

const shipmail = new ShipMailClient({ apiKey: process.env.SHIPMAIL_API_KEY! });

const message = await shipmail.messages.send({
  mailbox_id: "mbx_...",
  to: [{ address: "[email protected]", name: "User" }],
  subject: "Hello",
  text: "Hi there",
  html: "<p>Hi there</p>",
});

The SDK does not auto-read environment variables. Pass the key explicitly.

You can also pass a key string directly:

const shipmail = new ShipMailClient("sm_live_...");

Configuration

const shipmail = new ShipMailClient({
  apiKey: process.env.SHIPMAIL_API_KEY!,
  baseUrl: "https://shipmail.to/api/v1",
  maxRetries: 2,
  timeout: 30_000,
  fetch: customFetch,
  defaultHeaders: { "x-app-name": "my-app" },
});

| Option | Type | Default | Description | | ---------------- | ------------------------ | ---------------------------- | --------------------------------------------------------------- | | apiKey | string | required | Shipmail API key (sm_live_...). | | baseUrl | string | https://shipmail.to/api/v1 | API base URL. | | maxRetries | number | 2 | Retry count on 5xx and 429. Total attempts is maxRetries + 1. | | timeout | number | 30_000 | Per-request timeout in ms. | | fetch | typeof fetch | globalThis.fetch | Custom fetch implementation. | | defaultHeaders | Record<string, string> | {} | Headers added to every request. |

Domains

await shipmail.domains.create({ name: "example.com" });
await shipmail.domains.list({ limit: 10 });
await shipmail.domains.get("dom_...");
await shipmail.domains.update("dom_...", { catch_all_mailbox_id: "mbx_..." });
await shipmail.domains.delete("dom_...");
await shipmail.domains.verify("dom_...");
await shipmail.domains.search({ keyword: "example" });
await shipmail.domains.register({
  name: "example.com",
  years: 1,
  contact: {
    first_name: "Jane",
    last_name: "Doe",
    address1: "1 Main St",
    /* ... */
  },
});

Mailboxes

await shipmail.mailboxes.create({
  domain_id: "dom_...",
  address: "hello",
  display_name: "Hello",
});
await shipmail.mailboxes.list({ domain_id: "dom_..." });
await shipmail.mailboxes.get("mbx_...");
await shipmail.mailboxes.update("mbx_...", { display_name: "New Name" });
await shipmail.mailboxes.resetPassword("mbx_...", { password: "NewPassword1" });
const folders = await shipmail.mailboxes.listFolders("mbx_...");
const folder = await shipmail.mailboxes.createFolder("mbx_...", {
  name: "VIP",
  parent_id: null,
});
await shipmail.mailboxes.updateFolder("mbx_...", folder.id, { name: "VIP Clients" });
await shipmail.mailboxes.deleteFolder("mbx_...", folder.id);
const identities = await shipmail.mailboxes.listIdentities("mbx_...");
const rules = await shipmail.mailboxes.getRules("mbx_...");
await shipmail.mailboxes.updateRules("mbx_...", { rules: rules.rules });
await shipmail.mailboxes.updateSpamFilter("mbx_...", { threshold: 8 });
await shipmail.mailboxes.delete("mbx_...");

await shipmail.mailboxes.updateAutoReply("mbx_...", {
  enabled: true,
  subject: "Out of office",
  body: "Back on Monday.",
  from_date: "2026-06-01",
  to_date: "2026-06-07",
});

Messages

await shipmail.messages.send({
  mailbox_id: "mbx_...",
  to: [{ address: "[email protected]" }],
  cc: [{ address: "[email protected]" }],
  subject: "Hello",
  text: "Hi there",
  html: "<p>Hi there</p>",
});

await shipmail.messages.list({ mailbox_id: "mbx_...", limit: 25 });
await shipmail.messages.get("msg_...");

await shipmail.messages.reply("msg_...", {
  to: [{ address: "[email protected]" }],
  text: "Thanks for your email.",
});

Threads

const threads = await shipmail.threads.list({ mailbox_id: "mbx_..." });
const threadId = threads.data[0].id;
const thread = await shipmail.threads.get(threadId);

await shipmail.threads.reply(threadId, {
  text: "Thanks for your email.",
});

Webhooks

const webhook = await shipmail.webhooks.create({
  url: "https://example.com/webhook",
  events: ["message.received", "message.sent"],
  description: "Incoming email handler",
});
// webhook.secret is returned only on creation. Store it now.

await shipmail.webhooks.list();
await shipmail.webhooks.get("whk_...");
await shipmail.webhooks.update("whk_...", { active: false });
await shipmail.webhooks.delete("whk_...");

await shipmail.webhooks.rotateSecret("whk_...");
await shipmail.webhooks.test("whk_...");
await shipmail.webhooks.listDeliveries("whk_...");

Supported event types:

message.received
message.sent
message.delivered
message.bounced
message.complained
domain.verified
domain.verification_failed
domain.degraded
org.reputation_warning
org.sending_throttled
org.sending_suspended
org.reputation_recovered

Suppressions

await shipmail.suppressions.list({ limit: 25 });
await shipmail.suppressions.remove("[email protected]");

Auto-paginate:

for await (const item of shipmail.suppressions.listAutoPaginating({ limit: 100 })) {
  console.log(item.email_address, item.reason);
}

Status

const status = await shipmail.status.get();

Pagination

List methods return { data, pagination } with cursor-based pagination:

const page = await shipmail.domains.list({ limit: 10 });
page.data; // Domain[]
page.pagination; // { next_cursor, has_more }

if (page.pagination.has_more) {
  const next = await shipmail.domains.list({
    cursor: page.pagination.next_cursor,
    limit: 10,
  });
}

Auto-paginate over all pages:

for await (const domain of shipmail.domains.listAutoPaginating({ limit: 25 })) {
  console.log(domain.name);
}

listAutoPaginating is available on domains, mailboxes, messages, threads, webhooks, webhooks.listDeliveriesAutoPaginating, and suppressions.

Webhook verification

Verify incoming webhook signatures without instantiating a client:

import { verifyWebhook, WebhookVerificationError } from "shipmail";

try {
  const event = await verifyWebhook(rawBody, request.headers, webhookSecret);
  event.event_type; // typed WebhookEventType union
  event.data;
} catch (err) {
  if (err instanceof WebhookVerificationError) {
    // signature mismatch, missing header, expired timestamp, etc.
  }
}

Next.js Route Handler example

App Router consumes request.text() to get the raw body. Do not parse to JSON before verifying.

// app/api/webhooks/shipmail/route.ts
import { verifyWebhook, WebhookVerificationError } from "shipmail";

export async function POST(request: Request) {
  const rawBody = await request.text();
  const secret = process.env.SHIPMAIL_WEBHOOK_SECRET!;

  try {
    const event = await verifyWebhook(rawBody, request.headers, secret);
    // handle event...
    return new Response("ok");
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response("invalid signature", { status: 401 });
    }
    throw err;
  }
}

Per-request options

Every method accepts a final options argument:

type MethodOptions = {
  timeout?: number;
  signal?: AbortSignal;
  headers?: Record<string, string>;
  idempotencyKey?: string;
};
await shipmail.messages.send(params, {
  timeout: 5_000,
  headers: { "x-trace-id": traceId },
});

Idempotency

Pass idempotencyKey on mutating calls to make them safe to retry:

await shipmail.messages.send(
  {
    mailbox_id: "mbx_...",
    to: [{ address: "[email protected]" }],
    subject: "Receipt",
    text: "Thanks for your purchase.",
  },
  { idempotencyKey: `receipt-${orderId}` },
);

The SDK adds the key as the Idempotency-Key header. Reuse the same key to retry without sending a duplicate email. Keys are scoped per API key.

Cancellation

Pass an AbortSignal to cancel an in-flight request. The signal also cancels SDK-internal retries:

const controller = new AbortController();
setTimeout(() => controller.abort(), 2_000);

await shipmail.messages.send(params, { signal: controller.signal });

Custom fetch and proxies

Inject a custom fetch implementation for proxies, observability, or testing:

const shipmail = new ShipMailClient({
  apiKey: process.env.SHIPMAIL_API_KEY!,
  fetch: async (url, init) => {
    const start = Date.now();
    const res = await fetch(url, init);
    metrics.histogram("shipmail.fetch.duration_ms", Date.now() - start);
    return res;
  },
});

The custom fetch receives the same arguments as the global fetch and must return a Response.

Errors

The SDK throws typed errors that map to HTTP responses. All inherit from ShipMailError:

import {
  ShipMailError,
  AuthenticationError,
  AuthorizationError,
  ValidationError,
  NotFoundError,
  ConflictError,
  RateLimitError,
  QuotaExceededError,
  InternalServerError,
  ConnectionError,
} from "shipmail";

try {
  await shipmail.messages.send(params);
} catch (err) {
  if (err instanceof ValidationError) {
    err.message;
    err.details; // field-level validation errors
  }
  if (err instanceof RateLimitError) {
    err.retryAfter; // seconds
  }
  if (err instanceof ShipMailError) {
    err.status; // HTTP status
    err.type; // error type string
    err.requestId; // include this when contacting support
    err.retryable;
  }
  throw err;
}

| Error | When | | --------------------- | -------------------------------------------------------------- | | AuthenticationError | 401. Bad or missing API key. | | AuthorizationError | 403. Key lacks permission for the resource. | | ValidationError | 400 or 422. See details for per-field errors. | | NotFoundError | 404. | | ConflictError | 409. Resource already exists or state conflict. | | RateLimitError | 429. Read retryAfter (seconds). | | QuotaExceededError | 402. Plan or sending quota exceeded. | | InternalServerError | 5xx. Retried automatically up to maxRetries. | | ConnectionError | Network error, timeout, or DNS failure. Retried automatically. |

Retries

The SDK retries on 5xx, 429, and connection errors with exponential backoff and jitter. Retry-After is honored when present. Default is 2 retries (3 total attempts).

new ShipMailClient({ apiKey, maxRetries: 0 }); // disable retries

Retries respect any AbortSignal you pass via MethodOptions.signal.

Bundling

The package ships ESM and CommonJS via exports, with "sideEffects": false for tree-shaking. Importing a single resource pulls in only what it needs. The published bundle has no runtime dependencies.

Testing

Mock by injecting a custom fetch at construction time:

const shipmail = new ShipMailClient({
  apiKey: "sm_live_test",
  fetch: async () =>
    new Response(JSON.stringify({ id: "msg_123", status: "queued" }), {
      status: 200,
      headers: { "content-type": "application/json" },
    }),
});

This is the recommended pattern for unit tests. No HTTP interception or mocking library required.

License

MIT.

Links