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.17.1

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 }

scheduledAt must be in the future and at most 30 days ahead; the API rejects a past or too-distant timestamp with 400. status is "scheduled" when scheduledAt is in the future, otherwise "queued".

Override the From identity per send (email only) with from (bare address) and/or fromName (display name). Either can be set on its own; both fall back to the provider connection's configured values. The same two fields work on sendRaw.

await senderkit.send({
  template: "welcome",
  to: "[email protected]",
  from: "[email protected]",   // bare address
  fromName: "Acme Support", // display name → "Acme Support <[email protected]>"
});

On managed sending, the from address is honored only when it's on the workspace's verified sending domain; fromName always applies. fromName is capped at 128 characters and may not contain control characters or angle brackets.

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. SMS recipients must be an E.164 phone number (e.g. +15555550123); anything else is rejected with 400 invalid_recipient.

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",
  search: "[email protected]", // substring match over id, recipient, template slug, and metadata
});

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

A returned Message carries engagement timestamps when the provider reports them: openedAt (first email open) and clickedAt (first link click). Both are ISO 8601 strings, or null until the event occurs. The matching message.opened and message.clicked webhook events are delivered as they happen (the message.clicked payload also includes the clicked link).

SenderKit also emits delivery-outcome webhook events you can subscribe an endpoint to: message.sent, message.delivered, message.failed, message.opted_out, message.complained, message.suppressed, and (for inbound mail) message.received. message.complained fires when the recipient marks the message as spam; a complaint emits both message.complained and message.opted_out, so handlers that only watch for opt-outs keep working unchanged. message.suppressed fires when a send is skipped because the address failed validation or was already on the suppression list after an earlier bounce or complaint — distinct from message.failed, which is a real bounce reported by the receiving mail server.

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