Skip to content

magictm/plunk-node-sdk

Repository files navigation

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.ts and 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-sdk

Quick start

import { 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>",
});

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 with plunk.track(...) (/v1/track).

Public API

// 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);

Contacts

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);
}

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: "qa@example.com" });
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: ["a@example.com", "b@example.com"],
});
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 }); // disable

Idempotency

await plunk.contacts.create(
  { email: "user@example.com" },
  { 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 test

License

MIT

About

Zero-dependency Node.js (>=18) SDK for the Plunk API.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors