Official TypeScript SDK for the Sendly REST API.
Type-safe email, contact, domain, template, webhook, and suppression operations. Generated from the public OpenAPI spec, so every endpoint and schema stays in sync.
This repository is the official standalone home and source of truth for the Sendly TypeScript SDK — issues and PRs are welcome here. Its surface is contract-tested against Sendly's public OpenAPI spec on every change, so the client never drifts from the live API. Full docs live at https://docs.sendly.now.
The package is not yet published to npm. Until the first release, install directly from GitHub:
npm install github:DevinoSolutions/sendly-js
# or
pnpm add github:DevinoSolutions/sendly-jsRequires Node 20+ (or any runtime with global fetch and AbortSignal.timeout).
The API base is https://api.sendly.now; full docs live at
https://docs.sendly.now.
import { Sendly } from "@sendly/sdk";
const sendly = new Sendly({ apiKey: process.env.SENDLY_API_KEY! });
await sendly.emails.send({
from: "hello@your-domain.com",
to: "user@example.com",
subject: "Welcome to Acme",
html: "<p>Glad to have you.</p>",
});Pass a project API key. sk_* keys allow full access; pk_* keys are
sending-only. Keys are sent in the Authorization: Bearer <key> header
on every request.
const sendly = new Sendly({
apiKey: "sk_live_...", // required
baseUrl: "https://api.sendly.now", // optional, override for staging / self-hosted
timeout: 30_000, // ms, optional (default 30s)
});const result = await sendly.emails.send(
{
from: "hello@your-domain.com",
to: "user@example.com",
subject: "Order confirmed",
html: "<p>Thanks for your order.</p>",
},
{ idempotencyKey: "order-confirm-12345" }, // optional, replays deduped 24h
);const page = await sendly.emails.list({ limit: 20, tag: "welcome", status: "DELIVERED" });
for (const email of page.data.items) {
console.log(email.id, email.to, email.status);
}
if (page.data.cursor) {
const next = await sendly.emails.list({ limit: 20, cursor: page.data.cursor });
}const contact = await sendly.contacts.upsert({
email: "user@example.com",
customFields: { plan: "pro", signedUpAt: new Date().toISOString() },
});const domain = await sendly.domains.create({ name: "mail.your-domain.com" });
await sendly.domains.verify(domain.id);
const status = await sendly.domains.getVerification(domain.id);const { webhook, secret } = await sendly.webhooks.create({
url: "https://your-app.com/webhooks/sendly",
eventTypes: ["email.delivered", "email.bounced", "email.complained"],
});
// store `secret` securely — used to verify HMAC signatures on incoming callsawait sendly.suppression.add({ email: "angry@example.com", reason: "MANUAL" });Records a custom event against a contact. Works with both sk_* and pk_*
keys (reserved system event names are rejected).
const tracked = await sendly.events.track({
event: "purchase.completed",
email: "user@example.com",
data: { plan: "pro", amount: 4900 },
});
console.log(tracked.contact, tracked.event);const check = await sendly.verify.email({ email: "user@example.com" });
if (!check.valid) {
console.log("rejecting", check.reason);
}Every non-2xx response throws a typed SendlyError subclass. Switch on the
class (no string matching needed):
import {
SendlyValidationError,
SendlyAuthenticationError,
SendlyNotFoundError,
SendlyRateLimitError,
SendlyServerError,
} from "@sendly/sdk";
try {
await sendly.emails.send({ from, to, subject, html });
} catch (err) {
if (err instanceof SendlyValidationError) {
console.warn("bad input:", err.errorCode, err.message);
} else if (err instanceof SendlyAuthenticationError) {
console.error("check your API key");
} else if (err instanceof SendlyRateLimitError) {
// back off and retry
} else if (err instanceof SendlyServerError) {
// 5xx — retry with exponential backoff
} else {
throw err;
}
}Each error exposes:
statusCode— HTTP status (0 for transport failures)errorCode— stable machine code from the API envelopemessage— human-readable messagebody— full parsed response body for debugging
Pass idempotencyKey on any write that supports it (emails.send,
emails.batch, contacts.create, contacts.upsert, contacts.bulkCreate)
to make retries safe. Replays within 24 hours return the original result
instead of acting twice.
await sendly.emails.send({ from, to, subject, html }, { idempotencyKey: `signup-${userId}` });Inject your own fetch for SSR, instrumentation, or testing:
const sendly = new Sendly({
apiKey: "sk_test",
fetch: async (input, init) => {
console.log("outbound", init?.method, input);
return globalThis.fetch(input, init);
},
});Full reference, schemas, and live OpenAPI spec live at https://docs.sendly.now.
pnpm install # install pinned toolchain
pnpm test # run the vitest suite once
pnpm lint # eslint (0 warnings tolerated)
pnpm check-types # tsc --noEmit
pnpm build # regenerate types from openapi.json, then bundle with tsupThe type definitions in src/types.generated.ts are generated from
openapi.json via pnpm build:types. openapi.json is a committed snapshot
of Sendly's public OpenAPI spec; refresh it from the live API with
pnpm sync-spec, then regenerate the types (pnpm build:types, which
pnpm build runs for you). The SDK surface is verified against this snapshot
by the contract suite in src/__tests__/contract.test.ts.
MIT © Devino Solutions