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

@senderkit/sdk

v0.10.0

Published

Official TypeScript SDK for SenderKit — notification infrastructure for modern SaaS apps.

Readme

@senderkit/sdk

npm version CI Coverage OpenSSF Scorecard

Official TypeScript SDK for SenderKit — notification infrastructure for modern SaaS apps. Send welcome emails, password resets, billing notifications, and other transactional messages with a single, predictable API.

  • ESM + CJS dual-publish — works with import and require
  • Zero runtime dependencies
  • Node.js 18+ (uses native fetch); runs on edge runtimes too via injectable fetch
  • Typed end-to-end with generated .d.ts / .d.cts
  • Safe by default — automatic retries, timeouts, and idempotency

Install

npm install @senderkit/sdk
# or
pnpm add @senderkit/sdk
# or
bun add @senderkit/sdk

Quick start

import { SenderKit } from "@senderkit/sdk";

const senderkit = new SenderKit({
  apiKey: process.env.SENDERKIT_API_KEY!,
});

await senderkit.send({
  template: "welcome",
  to: "[email protected]",
  vars: { name: "John" },
});

Sending

send

const { id } = await senderkit.send({
  template: "welcome",            // template slug
  to: "[email protected]",         // recipient
  vars: { name: "John" },        // template variables
  channel: "email",               // optional; defaults to template's primary channel
  idempotencyKey: "welcome-u123", // optional; prevents duplicate sends on retry
});

Defer delivery with scheduledAt — accepts an ISO 8601 string or a Date:

await senderkit.send({
  template: "trial-ending",
  to: "[email protected]",
  scheduledAt: "2026-06-01T09:00:00Z",
});

The response shape:

{ id: "msg_…", status: "queued" | "scheduled", livemode: boolean }

status is "scheduled" when scheduledAt is in the future, otherwise "queued".

sendRaw

Send inline content without registering a template — useful for one-off admin notifications, contact-form replies, AI-generated drafts, or any case where the body is known at call-time.

await senderkit.sendRaw({
  channel: "email",
  to: "[email protected]",
  content: {
    subject: "Your receipt",
    html: "<p>Thanks for your order.</p>",
  },
  metadata: { source: "checkout" },
});

SMS, push, and web-push work the same way — switch the channel and the content shape:

await senderkit.sendRaw({
  channel: "sms",
  to: "+15555550123",
  content: { body: "Your code is 123456" },
});

await senderkit.sendRaw({
  channel: "push",
  to: "ExponentPushToken[xxx]",
  content: { title: "Shipped", body: "Tracking #ABC", badge: 1 },
});

// Web push (browser). `to` is the JSON-encoded browser PushSubscription.
await senderkit.sendRaw({
  channel: "web-push",
  to: JSON.stringify(subscription), // { endpoint, keys: { p256dh, auth } }
  content: {
    title: "Back in stock",
    body: "The item you wanted is available.",
    icon: "https://app.example.com/icon-192.png",
    clickUrl: "https://app.example.com/product/42",
  },
});

Raw sends accept the same retry, idempotency, and error-handling behavior as template sends. Pass interpolate: true together with vars to opt into server-side variable substitution inside content. Add scheduledAt to defer delivery, same as with send.

sendBatch

Fan out many messages at once. Returns one result per item — failures don't abort the batch. A batch can mix template and raw items freely.

const results = await senderkit.sendBatch([
  { template: "welcome", to: "[email protected]", vars: { name: "John" } },
  { template: "trial-ending", to: "[email protected]", vars: { daysLeft: 3 } },
]);

for (const r of results) {
  if (r.ok) console.log("sent", r.id);
  else console.error("failed", r.index, r.error.message);
}

Options:

await senderkit.sendBatch(messages, {
  concurrency: 5,             // max parallel requests (default 5)
  idempotencyKey: "cohort-1", // each item gets `cohort-1-0`, `cohort-1-1`, …
});

Live vs test mode

The mode is encoded in your API key — no flag to set:

  • sk_live_… → production / live mode
  • sk_test_… → test mode

Switch environments by swapping the key.

Inspect the client's mode before sending — useful for dev-environment guardrails:

const senderkit = new SenderKit({ apiKey: process.env.SENDERKIT_API_KEY! });

if (process.env.NODE_ENV === "production" && senderkit.mode === "test") {
  throw new Error("refusing to boot prod with a test key");
}

mode is "test" when the key starts with sk_test_, otherwise "live".

Idempotency

Every send call includes an Idempotency-Key header. If you don't pass one, the SDK auto-generates a UUID per call so transparent retries (network blips, 429s, 5xx) never duplicate a send. Pass your own key when you need end-to-end deduplication across your own retries:

await senderkit.send({
  template: "invoice-paid",
  to: "[email protected]",
  vars: { invoice: "inv_123" },
  idempotencyKey: `invoice-paid:inv_123`,
});

Error handling

All errors extend SenderKitError. Use instanceof to branch:

import {
  SenderKitAuthenticationError,
  SenderKitNetworkError,
  SenderKitPermissionError,
  SenderKitRateLimitError,
  SenderKitTimeoutError,
  SenderKitValidationError,
} from "@senderkit/sdk";

try {
  await senderkit.send({ template: "welcome", to: "[email protected]" });
} catch (err) {
  if (err instanceof SenderKitValidationError) {
    console.error("Bad request:", err.message, err.issues);
  } else if (err instanceof SenderKitAuthenticationError) {
    console.error("Check your API key");
  } else if (err instanceof SenderKitPermissionError) {
    console.error("API key is missing the required scope:", err.message);
  } else if (err instanceof SenderKitRateLimitError) {
    console.warn(`Rate limited, retry after ${err.retryAfter}ms`);
  } else if (err instanceof SenderKitTimeoutError) {
    console.warn("Request timed out");
  } else if (err instanceof SenderKitNetworkError) {
    console.warn("Network error", err.cause);
  } else {
    throw err;
  }
}

| Class | When it fires | | --- | --- | | SenderKitValidationError | 400 / 422 — invalid request, includes issues from the API | | SenderKitAuthenticationError | 401 — missing or invalid API key | | SenderKitPermissionError | 403 — key is valid but lacks the required scope (code: "insufficient_scope") | | SenderKitRateLimitError | 429 — includes retryAfter (ms) | | SenderKitApiError | other 4xx / 5xx after retries are exhausted | | SenderKitTimeoutError | request exceeded the configured timeout | | SenderKitNetworkError | fetch threw (DNS, connection refused, …) | | SenderKitError | base class — catch this to handle them all |

SenderKitPermissionError extends SenderKitApiError (not SenderKitAuthenticationError), so a catch (SenderKitApiError) still handles it.

Scopes

API keys can be restricted to a least-privilege set of scopes. A key minted without explicit scopes is unscoped and has full access (the default), so existing keys are unaffected. A scoped key only authorizes the matching operations:

| Scope | Authorizes | | --- | --- | | read | templates.list / templates.get, messages.list / messages.get, context() | | send | send, sendRaw, sendBatch | | cancel | messages.cancel |

Calling an operation outside a scoped key's grant returns 403 with code: "insufficient_scope" — surfaced as SenderKitPermissionError. The ApiScope type ("read" | "send" | "cancel") is exported for convenience.

Templates and messages

const templates = await senderkit.templates.list();
const welcome = await senderkit.templates.get("welcome");

const { data, nextCursor } = await senderkit.messages.list({
  limit: 50,
  status: "delivered",
  template: "welcome",
});

const message = await senderkit.messages.get("msg_…");

Next.js route handler

// app/api/welcome/route.ts
import { NextResponse } from "next/server";
import { SenderKit } from "@senderkit/sdk";

const senderkit = new SenderKit({ apiKey: process.env.SENDERKIT_API_KEY! });

export async function POST(req: Request) {
  const { email, name } = (await req.json()) as { email: string; name: string };
  const result = await senderkit.send({
    template: "welcome",
    to: email,
    vars: { name },
    idempotencyKey: `welcome:${email}`,
  });
  return NextResponse.json(result, { status: 202 });
}

React Email

Use @senderkit/react-email to wrap React Email components with SenderKit metadata (id, subject, preview data, optional schema) and render them to HTML. The two packages are designed to be used together.

API reference

new SenderKit(options)

| Option | Type | Default | Description | | --- | --- | --- | --- | | apiKey | string | — | Required. sk_live_… or sk_test_…. | | baseUrl | string | https://api.senderkit.com | Override the API endpoint. | | timeout | number | 30000 | Per-request timeout in ms. | | maxRetries | number | 2 | Retries on network / timeout / 429 / 5xx. | | fetch | typeof fetch | globalThis.fetch | Inject a custom fetch (tests, edge runtimes). |

Methods

| Method | Returns | | --- | --- | | send(request) | Promise<SendResponse> | | sendRaw(request) | Promise<SendResponse> | | sendBatch(requests, options?) | Promise<BatchSendResult[]> | | templates.list() | Promise<Template[]> | | templates.get(slug) | Promise<Template> | | messages.list(params?) | Promise<{ data: Message[]; nextCursor: string \| null }> | | messages.get(id) | Promise<Message> |

License

MIT