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

samva

v0.3.0

Published

TypeScript SDK for the Samva email API

Readme

samva

TypeScript SDK for Samva's public customer API.

Documentation

See AGENTS.md for concise guidance when using this package with coding agents.

Overview

The root entrypoint is Promise-based:

import { createClient } from "samva";

The same Promise SDK is also available at samva/promises. Native Effect users import domain modules such as samva/effect/email and provide samva/effect/client. Both clients are generated from Samva's public OpenAPI document, while the Effect runtime does not wrap or execute the Promise client. Effect is an optional peer dependency, so Promise-only applications do not install or load it.

Installation

npm install samva
# or
bun add samva
# or
yarn add samva

Quick Start

import { createClient } from "samva";

const client = createClient({
  baseUrl: "https://api.samva.dev",
  apiKey: "<samva-api-key>",
});

const contact = await client.contacts.findOrCreate({
  name: "Ada Lovelace",
  email: "[email protected]",
});

const message = await client.messages.send({
  to: [contact],
  channel: "email",
  email: {
    subject: "Hello",
    html: "<p>Hello World</p>",
    text: "Hello World",
  },
  metadata: {
    externalUserId: "user_123",
  },
});

const messages = await client.messages.list({
  page: "1",
  limit: "20",
});

Effect Quick Start

Install effect next to samva when using the Effect entrypoint:

bun add samva [email protected]
import { Effect } from "effect";
import * as Client from "samva/effect/client";
import * as Email from "samva/effect/email";

const program = Email.send({
  to: "[email protected]",
  subject: "Welcome",
  html: "<h1>Welcome to Samva</h1>",
}).pipe(Effect.provide(Client.layerFetch({ apiKey: "<samva-api-key>" })));

const message = await Effect.runPromise(program);

Authentication

Samva supports two client auth modes. Use one per createClient instance.

API key (automation, CI, single org)

const client = createClient({
  apiKey: "samva_sk_live_...",
  baseUrl: "https://api.samva.dev",
});
  • X-API-Key is configured automatically from apiKey.
  • Organization context is derived from the API key — do not send x-org-slug.

OAuth bearer (interactive, multi-org)

const client = createClient({
  authToken: sessionAccessToken,
  baseUrl: "https://api.samva.dev",
  headers: { "x-org-slug": "your-org-slug" },
});
  • Requests use Authorization: Bearer <authToken>.
  • The token is user-scoped; send the target organization slug on every request via the x-org-slug header.
  • There is no SDK orgSlug helper — pass the header directly.

For terminal workflows (samva login, samva org use, samva email send --dry-run), see the @samva/cli README.

Service Surface

The Promise client exposes:

  • apiKeys
  • campaigns
  • contacts
  • conversations
  • design
  • email
  • media
  • messages
  • operations
  • organizations
  • scheduledMessages
  • templates
  • webhooks

The Effect SDK exposes Pascal-cased domain modules with lower-camel operations:

  • ApiKeys
  • Campaigns
  • Contacts
  • Conversations
  • Design
  • Email
  • Media
  • Messages
  • Operations
  • Organizations
  • ScheduledMessages
  • Templates
  • Webhooks

Effect operations take direct parameter objects: body, path, and query fields are not wrapped in payload or params envelopes. Import the modules you use from samva/effect/<lower-kebab> and provide Client.layerFetch once at the program boundary. Client.raw exposes the underlying generated client for advanced access, but ordinary workflows do not require it.

Email attachments use organization-owned media. Call media.create, upload the bytes with the returned instruction, call media.complete, and pass the ready mediaId to email.send or messages.send. A message accepts at most 10 attachments totaling 25 MiB of raw bytes; the same budget covers inline base64 content and owned media references. Remote URL attachments are not.

Effect failures carry Samva's real tagged errors (RateLimitedError, ValidationError, PaymentRequiredError, …) directly in the error channel, so Effect.catchTag("RateLimitedError", …) works against any operation without enumerating status codes, and each error exposes its own fields (no wrapper cause). The SDK also exports category helpers such as isRetryable, isTransient, catchAuthError, and catchTransient. Success values decode to domain types: annotated absolute instants become Date, sensitive fields become Redacted, and unannotated wire timestamps remain strings.

Default-on retry

The Effect client retries safe operations out of the box, so you do not wrap calls in Effect.retry:

  • What retries — read operations (GET/HEAD) and sends. Sends are safe because the client attaches an Idempotency-Key (below), so the server deduplicates a replayed attempt. Every other mutating operation (create, update, remove, …) is keyless and is never auto-retried.
  • When — only for isRetryable failures: throttling (429), transient server errors (including a transient BillingProviderError), and request-transport failures. ValidationError, ConflictError, NotFound, auth, and billing-limit PaymentRequiredError failures never retry.
  • How — jittered exponential backoff, bounded to four attempts total. A 429 honors the server's retryAfterSeconds hint (capped at 60s) in place of the computed delay.

Retry policy is read from context via the Retry.Service service. When it is absent, the default applies, so no operation gains a requirement. Override it with a Layer:

import { Effect } from "effect";
import { isRetryable } from "samva/effect/categories";
import * as Retry from "samva/effect/retry";

// Turn auto-retry off for a program:
program.pipe(Effect.provide(Retry.layerDisabled));

// Or install a custom policy (any Effect.retry options):
program.pipe(Effect.provide(Retry.layer({ times: 6, while: isRetryable })));

Idempotency keys

Each messages.send / email.send call generates an Idempotency-Key per logical call — stable across the built-in retries, distinct across calls — so a retried send never delivers twice. Pass an explicit key for cross-process deduplication (safe to replay the same request from a different machine):

const send = Email.send(input, { idempotencyKey: "order-4417-receipt" });

Reusing a key with identical content replays the original response; reusing it with different content fails with ConflictError.

Stream pagination

Every list operation that returns the uniform { items, pagination: { page, limit, total, totalPages } } envelope carries two Stream companions, so you traverse pages without hand-rolling a page loop:

  • list.items(input?) — a Stream of individual items across all pages.
  • list.pages(input?) — a Stream of raw page envelopes.
import { Stream } from "effect";
import * as Contacts from "samva/effect/contacts";

// Every contact, one page fetched at a time:
const allContacts = Stream.runCollect(Contacts.list.items());

// Bounded consumption is lazy — this fetches only as many pages as it needs:
const firstFifty = Stream.runCollect(Contacts.list.items().pipe(Stream.take(50)));

// Caller params (start page, limit, filters) are respected:
const activeFrom2 = Contacts.list.items({ page: "2", limit: "100", status: "active" });

The companions are attached uniformly to every paginated list, including Messages.list, Contacts.list, Campaigns.list, Email.listDomains, and Webhooks.list. Each page fetch reuses the ambient Retry policy. Reads retry per page; pagination does not nest retries. Page and limit inputs retain their OpenAPI wire-string representation. The Promise client is unchanged; stream pagination is Effect-only.

Redacted secrets

Secret-bearing response fields — the plaintext API key returned by apiKeys.create / apiKeys.rotate, and the webhook signing secret from webhooks.regenerateSecret — decode to Redacted<string> on the Effect surface. The secret never leaks through logs, String(...), or JSON.stringify; unwrap it explicitly with Redacted.value when you need the plaintext:

import { Effect, Redacted } from "effect";
import * as ApiKeys from "samva/effect/api-keys";

const inspectKey = Effect.gen(function* () {
  const created = yield* ApiKeys.create({ name: "CI" });
  `${created.key}`; // "<redacted>" — safe to log
  return Redacted.value(created.key); // the plaintext key, unwrapped on purpose
});

The wire body and the Promise client keep plain strings; only the Effect surface is Redacted.

OpenAPI vendor extensions

The published openapi.json carries machine-readable semantics on top of the standard schema, derived from the same contract annotations the Effect SDK uses (no second taxonomy):

| Extension | Where | Meaning | | -------------- | ----------------- | ------------------------------------------------------------------------------------------------- | | x-category | error schemas | The error's semantic categories (e.g. ["throttling", "transient"]). Omitted when uncategorized. | | x-retryable | error schemas | true when the error is safe to retry. | | x-idempotent | operations | true when the operation accepts an Idempotency-Key header. | | x-sensitive | schema properties | true for secret-bearing fields (plain string on the wire; Redacted in the Effect SDK). |

These let MCP and third-party consumers read retry/idempotency/secret semantics straight from the spec.

Usage Examples

Send Email Through Unified Messages

const contact = await client.contacts.findOrCreate({
  name: "Ada Lovelace",
  email: "[email protected]",
});

const response = await client.messages.send({
  to: [contact],
  channel: "email",
  email: {
    subject: "Welcome",
    html: "<h1>Welcome to Samva</h1>",
    text: "Welcome to Samva",
  },
  metadata: {
    campaign: "welcome",
  },
});

Native Effect Send Email

import * as Email from "samva/effect/email";

const message =
  yield *
  Email.send({
    to: "[email protected]",
    subject: "Welcome",
    html: "<h1>Welcome to Samva</h1>",
  });

List Conversations

const response = await client.conversations.list({
  page: "1",
  limit: "20",
  status: "active",
});

Schedule a Message

const scheduled = await client.scheduledMessages.create({
  send: {
    to: [{ email: "[email protected]" }],
    channel: "email",
    email: { subject: "Reminder", html: "<p>See you tomorrow.</p>" },
  },
  scheduledFor: "2026-08-01T09:00:00Z",
});

// Later, cancel it before it dispatches
await client.scheduledMessages.cancel({ id: scheduled.id });

Create and Run a Campaign

const campaign = await client.campaigns.create({
  name: "August newsletter",
  channel: "email",
  content: {
    channel: "email",
    email: { subject: "What's new", html: "<h1>Updates</h1>" },
  },
  audience: { includeTags: ["subscribers"], excludeTags: ["bounced"] },
});

const run = await client.campaigns.scheduleRun({
  id: campaign.id,
  scheduledFor: "2026-08-01T09:00:00Z",
});

const recipients = await client.campaigns.listRecipients({
  id: campaign.id,
  runId: run.id,
});

Custom Headers

const client = createClient({
  apiKey: "<samva-api-key>",
  headers: {
    "X-Custom-Header": "value",
  },
});

Error Handling

import { RateLimitedError, SamvaApiError, SamvaTransportError } from "samva";

try {
  const message = await client.email.send({
    to: "[email protected]",
    subject: "Test",
    html: "<p>Test</p>",
  });
  console.log(message.id);
} catch (error) {
  if (error instanceof RateLimitedError) {
    console.error(`Retry after ${error.retryAfterSeconds} seconds`);
  } else if (error instanceof SamvaApiError) {
    console.error(error._tag, error.status, error.message);
  } else if (error instanceof SamvaTransportError) {
    console.error("Transport failure", error.cause);
  } else {
    throw error;
  }
}

Every ergonomic method returns its decoded success value and throws a generated SamvaApiError subclass for failures declared by OpenAPI. Network and malformed-response failures throw SamvaTransportError.

Raw response access

const result = await client.raw.messages.send({
  body: {
    to: [{ contactId: "contact-id" }],
    channel: "email",
    email: { subject: "Hello", text: "Hello" },
  },
});

if (result.error) console.error(result.response?.status, result.error);

Use client.raw only when you need the generated { data, error, request, response } envelope or transport-level request options.

Type Safety

All request and response types are generated from OpenAPI schemas:

import type { MessagesSendData } from "samva";

const requestData: MessagesSendData = {
  body: {
    to: [{ contactId: "contact-id" }],
    channel: "email",
    email: {
      subject: "Hello",
      html: "<p>Hello World</p>",
    },
  },
};

const response = await client.messages.send(requestData.body);