TypeScript SDK
The official shipmail package provides type-safe access to the full API. Works in Node.js 18+, Bun, Deno, and edge runtimes.
Last updated
Installation
npm install shipmailbun add shipmailQuick start
import ShipMail from "shipmail";
const shipmail = new ShipMail(process.env.SHIPMAIL_API_KEY);
// Send an email
const message = await shipmail.messages.send({
from: "hello@yourdomain.com",
to: ["recipient@example.com"],
subject: "Hello from shipmail",
text: "It works.",
});
console.log(message.id); // msg_...Configuration
Pass a string for the API key alone, or an object for full control:
const shipmail = new ShipMail({
apiKey: process.env.SHIPMAIL_API_KEY,
baseUrl: "https://shipmail.to/api/v1", // default
maxRetries: 2, // default
timeout: 30_000, // ms, default
defaultHeaders: { "X-Custom": "value" },
});Resources
The client exposes each API resource as a property:
Newsletter workflow
Use blocks for editable newsletters. Prose fields inside blocks are plain text; use newlines for paragraph breaks. Use body_html or custom_html only when you need raw HTML.
import { readFile } from "node:fs/promises";
const newsletterDomains = await shipmail.newsletters.domains.list({ limit: 25 });
const assets = await shipmail.newsletters.assets.list({ kind: "image", limit: 25 });
const image = await shipmail.newsletters.assets.upload({
filename: "feature.png",
content_type: "image/png",
data: await readFile("feature.png"),
});
const existingImage = await shipmail.newsletters.assets.registerFromUrl({
url: "https://cdn.shipmail.to/newsletter-images/org_123/demo.png",
filename: "demo.png",
});
console.log(assets.storage.used_bytes, assets.storage.limit_bytes);
const newsletter = await shipmail.newsletters.create({
audience_id: "aud_...",
newsletter_domain_id: newsletterDomains.data[0].id,
name: "July changelog",
subject: "What shipped in July",
preview_text: "A quick product update",
blocks: [
{ type: "heading", level: 1, text: "July updates" },
{ type: "callout", variant: "info", title: "Quick note", body: "A short intro." },
{ type: "paragraph", body: "Use blank lines for paragraph breaks." },
{ type: "image", url: image.url, alt: "Product screenshot" },
{ type: "image", url: existingImage.url, alt: "Existing CDN screenshot" },
{
type: "columns",
ratio: "50-50",
left: { title: "For teams", body: "Shared inbox improvements." },
right: { title: "For agents", body: "API and MCP improvements." },
},
],
});
await shipmail.newsletters.preview(newsletter.id);
await shipmail.newsletters.sendTest(newsletter.id, {
recipient_email: "owner@example.com",
});
await shipmail.newsletters.preflight(newsletter.id);
await shipmail.newsletters.schedule(newsletter.id, {
scheduled_at: "2026-08-01T09:00:00.000Z",
});Preflight returns a URL breakdown. Links, images, and video thumbnails count toward the 20 unique URL limit.
Error handling
API errors throw typed subclasses of ShipMailError. Each error exposes status, type, requestId, and retryable. Newsletter guardrail failures on test-send and schedule are ValidationErrors with preflight fields in details.
import ShipMail, {
ValidationError,
RateLimitError,
ShipMailError,
} from "shipmail";
try {
await shipmail.messages.send({ ... });
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.details); // per-field errors
} else if (err instanceof RateLimitError) {
console.log(err.retryAfter); // seconds to wait
} else if (err instanceof ShipMailError) {
console.log(err.status, err.requestId);
}
}Pagination
List methods return a Page object that implements AsyncIterable. Iterate with for await to automatically fetch all pages:
const domains = shipmail.domains.list();
for await (const domain of domains) {
console.log(domain.name);
}Pass limit and cursor for manual control:
const page = shipmail.domains.list({ limit: 10 });Webhook verification
The SDK exports a standalone verifyWebhook function. It is async because it dynamically imports the crypto module for portability.
import { verifyWebhook } from "shipmail";
const event = await verifyWebhook(
rawBody,
request.headers,
process.env.WEBHOOK_SECRET,
);
console.log(event.event_type); // "message.received"Throws WebhookVerificationError if the signature is invalid or the timestamp is outside the 5-minute tolerance window. Pass { toleranceInSeconds: 600 } to customize.
Per-request options
Every method accepts an optional last argument for request-level overrides:
await shipmail.domains.create(
{ name: "example.com" },
{
idempotencyKey: "550e8400-...",
timeout: 10_000,
signal: AbortSignal.timeout(5000),
},
);Links
- npm
- Python SDK (sync and async clients for Python 3.10+).
- REST API reference for direct HTTP usage.