shipmail
v0.1.23
Published
Official TypeScript SDK for the ShipMail API
Downloads
927
Maintainers
Readme
Shipmail TypeScript SDK
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
- Quick start
- Configuration
- Domains
- Mailboxes
- Messages
- Threads
- Webhooks
- Suppressions
- Status
- Pagination
- Webhook verification
- Per-request options
- Idempotency
- Cancellation
- Custom fetch and proxies
- Errors
- Retries
- Bundling
- Testing
- License
- Links
Install
bun add shipmail
# or
npm install shipmail
# or
pnpm add shipmailQuick 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_recoveredSuppressions
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 retriesRetries 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.
