plunk-node-sdk
v0.2.2
Published
Zero-dependency Node.js (>=18) SDK for the Plunk API.
Maintainers
Readme
plunk-node-sdk
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.
Install
pnpm add plunk-node-sdk
# or
npm install plunk-node-sdkQuick start
import { Plunk } from "plunk-node-sdk";
const plunk = new Plunk(process.env.PLUNK_SECRET_KEY!);
await plunk.send({
to: "[email protected]",
subject: "Welcome",
body: "<p>Hello from Plunk!</p>",
});Authentication
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).
Public API
// Send a transactional email
await plunk.send({
to: "[email protected]",
subject: "Hi",
body: "<p>Hello</p>",
});
// Track an event
await plunk.track({ event: "signed_up", email: "[email protected]" });
// Verify an email
const result = await plunk.verify({ email: "[email protected]" });
console.log(result.valid, result.isDisposable);Contacts
const page = await plunk.contacts.list({ limit: 50 });
const contact = await plunk.contacts.create({
email: "[email protected]",
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);
}Templates
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" });Campaigns
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: "[email protected]" });
await plunk.campaigns.send(c.id); // or schedule with { scheduledAt: "..." }
const stats = await plunk.campaigns.stats(c.id);Segments
const seg = await plunk.segments.create({ name: "VIPs", type: "static" });
await plunk.segments.addMembers(seg.id, {
emails: ["[email protected]", "[email protected]"],
});
for await (const member of plunk.segments.listAllContacts(seg.id)) {
console.log(member.email);
}Workflows
for await (const wf of plunk.workflows.listAll()) {
console.log(wf.name);
}
const execs = await plunk.workflows.listExecutions("wf_id", { limit: 50 });Events
const names = await plunk.events.names();
for await (const evt of plunk.events.listAll()) {
console.log(evt.name, evt.email);
}Domains
const domains = await plunk.domains.list();
const added = await plunk.domains.create({ name: "mail.acme.com" });
await plunk.domains.delete(added.id);Error handling
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.
Timeouts & cancellation
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 });Retries
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 }); // disableIdempotency
await plunk.contacts.create(
{ email: "[email protected]" },
{ idempotencyKey: crypto.randomUUID() },
);Custom fetch
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);
},
});TypeScript
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";Development
pnpm install
pnpm typecheck
pnpm build
pnpm testLicense
MIT
