Zero-dependency Node.js SDK for the Plunk API.
- Zero runtime dependencies — uses only Node built-ins (
fetch,AbortSignal,URL). - Full API coverage — public API (
/v1/send,/v1/track,/v1/verify) plus contacts, templates, campaigns, segments, workflows, events, and domains. - TypeScript-first — fully typed inputs and responses; ships
.d.tsand ESM.js. - Auto-retry, auto-paginate, abort/timeout — sensible defaults, all configurable.
Requires Node.js 18 or newer (built-in
fetch+AbortSignal.timeout). ESM only.Tested on Node 18, 20, 22, and 24.
pnpm add plunk-node-sdk
# or
npm install plunk-node-sdkimport { Plunk } from "plunk-node-sdk";
const plunk = new Plunk(process.env.PLUNK_SECRET_KEY!);
await plunk.send({
to: "user@example.com",
subject: "Welcome",
body: "<p>Hello from Plunk!</p>",
});Pass either a string or an options object:
new Plunk("sk_your_secret_key");
new Plunk({
apiKey: "sk_your_secret_key",
baseUrl: "https://next-api.useplunk.com", // override (e.g. self-hosted)
timeoutMs: 30_000,
maxRetries: 2,
userAgent: "my-app/1.0",
fetch: globalThis.fetch, // inject a custom fetch
});sk_*keys work for every endpoint.pk_*public keys work only withplunk.track(...)(/v1/track).
// Send a transactional email
await plunk.send({
to: "user@example.com",
subject: "Hi",
body: "<p>Hello</p>",
});
// Track an event
await plunk.track({ event: "signed_up", email: "user@example.com" });
// Verify an email
const result = await plunk.verify({ email: "user@example.com" });
console.log(result.valid, result.isDisposable);const page = await plunk.contacts.list({ limit: 50 });
const contact = await plunk.contacts.create({
email: "jane@example.com",
subscribed: true,
data: { plan: "pro" },
});
await plunk.contacts.update(contact.id, { subscribed: false });
await plunk.contacts.delete(contact.id);
// Iterate every contact across every page
for await (const c of plunk.contacts.listAll()) {
console.log(c.email);
}const tpl = await plunk.templates.create({
name: "Welcome",
subject: "Welcome to Acme",
body: "<p>Hi {{name}}</p>",
type: "transactional",
});
await plunk.templates.update(tpl.id, { subject: "Welcome aboard" });const c = await plunk.campaigns.create({
name: "Spring promo",
subject: "20% off",
body: "<p>...</p>",
segments: ["seg_xyz"],
});
await plunk.campaigns.test(c.id, { email: "qa@example.com" });
await plunk.campaigns.send(c.id); // or schedule with { scheduledAt: "..." }
const stats = await plunk.campaigns.stats(c.id);const seg = await plunk.segments.create({ name: "VIPs", type: "static" });
await plunk.segments.addMembers(seg.id, {
emails: ["a@example.com", "b@example.com"],
});
for await (const member of plunk.segments.listAllContacts(seg.id)) {
console.log(member.email);
}for await (const wf of plunk.workflows.listAll()) {
console.log(wf.name);
}
const execs = await plunk.workflows.listExecutions("wf_id", { limit: 50 });const names = await plunk.events.names();
for await (const evt of plunk.events.listAll()) {
console.log(evt.name, evt.email);
}const domains = await plunk.domains.list();
const added = await plunk.domains.create({ name: "mail.acme.com" });
await plunk.domains.delete(added.id);Every non-success response throws a typed PlunkError:
import { PlunkError } from "plunk-node-sdk";
try {
await plunk.contacts.create({ email: "not-an-email" });
} catch (err) {
if (err instanceof PlunkError) {
console.error(err.code); // "VALIDATION_ERROR"
console.error(err.statusCode); // 422
console.error(err.requestId);
console.error(err.errors); // field-level details
console.error(err.suggestion);
} else {
throw err;
}
}Synthetic codes used for client-side failures: TIMEOUT, NETWORK_ERROR, INVALID_RESPONSE.
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5_000);
await plunk.contacts.list({ limit: 100 }, { signal: ctrl.signal });
// Per-call timeout (overrides client default)
await plunk.events.names({ timeoutMs: 2_000 });Transient failures (429, 5xx, network errors, timeouts) are retried up to
maxRetries times (default 2) with exponential backoff + jitter. The
Retry-After header is honored when present.
new Plunk({ apiKey: "sk_…", maxRetries: 0 }); // disableawait plunk.contacts.create(
{ email: "user@example.com" },
{ idempotencyKey: crypto.randomUUID() },
);Inject your own fetch (e.g. for proxies, tracing, mocking in tests):
import { Plunk } from "plunk-node-sdk";
const plunk = new Plunk({
apiKey: "sk_…",
fetch: async (url, init) => {
console.log("→", init?.method ?? "GET", url);
return fetch(url, init);
},
});Every method is fully typed. Resource interfaces (e.g. Contact, Campaign,
SendEmailParams) are exported from the package root.
import type {
Contact,
Plunk,
PlunkError,
SendEmailParams,
} from "plunk-node-sdk";pnpm install
pnpm typecheck
pnpm build
pnpm testMIT