npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

plunk-node-sdk

v0.2.2

Published

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

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.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: "[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 with plunk.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 }); // disable

Idempotency

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 test

License

MIT