Reference

SDKs

Official libraries for Node/TypeScript, Python, and Go, plus @sendara/react-email for authoring emails as JSX. Every SDK is a thin, typed wrapper over the same HTTP API — with idempotent retries, auto-pagination, and webhook verification built in.

There are four official packages, all at v0.2.0 and tracking the same API surface:

  • sendara (Node / TypeScript) — npm install sendara. First-class types, ESM + CJS, works in Node 18+ and modern edge runtimes.
  • sendara (Python) — pip install sendara. A synchronous Sendara client and an AsyncSendara client that share one typed model layer.
  • sendara-go (Go) — go get github.com/sendara/sendara-go. Idiomatic, context-aware, functional options for config.
  • @sendara/react-email — author emails as React components (JSX) and render them to HTML to pass straight to emails.send.
Prefer the raw HTTP API or a language we don't ship yet? Every SDK is a thin wrapper over the same endpoints — see the API reference. Authentication is always Authorization: Bearer sk_live_… (or sk_test_… in the sandbox).

Install

Quickstart

Construct a client with your API key, then send. The client reads SENDARA_API_KEYfrom the environment if you don't pass a key explicitly. Sends are idempotent by default — the SDK attaches a generated idempotency_key so transparent retries never double-send.

Python reserves from, so the email helper takes from_. It maps to the from_email the API expects — the sender must be on a verified domain.

Client configuration

Every client accepts the same three knobs: baseUrl, timeout, and maxRetries. Defaults are sensible — you rarely need to touch them outside tests or a self-hosted gateway.

apiKeystringRequired
Your secret key — sk_live_… in production, sk_test_… for the sandbox. The first positional argument; falls back to the SENDARA_API_KEY env var if omitted.
baseUrlstringOptional
API origin. Defaults to https://api.sendara.dev. Point it at a proxy or gateway in front of the API if you have one.
timeoutnumberOptional
Per-request timeout. Node milliseconds (default 30000), Python/Go seconds (default 30). A timeout is retried like any transient failure.
maxRetriesnumberOptional
How many times to retry 429 and 5xx responses with exponential backoff and jitter. Default 3. Set to 0 to disable client-side retries.
Retries apply to 429 and 5xx only, with exponential backoff and full jitter. A 429 honors the Retry-After header; rate-limit headers (X-RateLimit-Remaining, X-RateLimit-Reset) are surfaced on responses so you can pace ahead of the limit. See rate limits.

Typed errors

Failed requests raise a typed exception that mirrors the API's { "error": { "code", "message" } } envelope. Every error carries status, code, message, and the requestId (from the X-Request-Id response header) for support tickets. Subclasses let you branch on the failure without string-matching code.

The error hierarchy maps onto the codes the API emits. Named subclasses exist for the ones you'll branch on most:

  • UnauthorizedError (401), ForbiddenError (403) — auth and scope problems.
  • InvalidRequestError (400), NotFoundError (404) — bad input or unknown resource.
  • RateLimitError (429) — exposes retryAfter in seconds.
  • RecipientSuppressedError (409), IdempotencyKeyReusedError (409) — send conflicts.
  • FromNotVerifiedError (403/422), SpendCapExceededError (402) — sending-policy blocks.

Anything not given a dedicated class still arrives as a SendaraError with the raw code intact — see the full list on the errors page. (Go uses a single *sendara.Error with a Code field and sendara.Code* constants rather than distinct types.)

Auto-pagination

List endpoints are cursor-paginated (limit up to 100, an opaque cursor, and a next_cursor in the response). The SDKs hide the cursor plumbing: iterate the list and it fetches each page lazily as you go.

Need the raw cursor — to resume a job or paginate from a stored position? Drop to a single page (messages.page() in Node) and read nextCursor / next_cursor yourself. Ordering is keyset over created_atdescending, so it's stable as new messages arrive.

Verifying webhooks

Each SDK ships a webhooks.verifyhelper so you don't hand-roll the HMAC. Give it the raw request body, the request headers, and your subscription's signing secret; it checks the Sendara-Signature against HMAC-SHA256(secret, "<Sendara-Timestamp>.<rawBody>") in constant time, enforces a five-minute timestamp tolerance to defeat replays, and returns the parsed, typed event. On a mismatch it raises a verification error.

The signature covers the exact bytes of the body. Capture the raw payload before any JSON middleware parses it (Next.js await req.text(), Express express.raw(), Flask request.get_data()) or verification will always fail. Full scheme and retry semantics live on the webhooks page.

Authoring emails with React

@sendara/react-email lets you write transactional emails as React components and render them to email-safe HTML you pass straight to emails.send. Components compile to inline-styled, table-based markup that survives the major mail clients, so you keep JSX ergonomics without fighting Outlook.

emails/Welcome.tsx
import {
  Html,
  Head,
  Body,
  Container,
  Heading,
  Text,
  Button,
} from "@sendara/react-email";

export function Welcome({ name, url }: { name: string; url: string }) {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: "#f6f6f6" }}>
        <Container>
          <Heading>Welcome, {name} 🎉</Heading>
          <Text>Thanks for joining Acme. Confirm your address to get going.</Text>
          <Button href={url}>Confirm email</Button>
        </Container>
      </Body>
    </Html>
  );
}

Render the component to HTML and send it. render returns a string of inlined HTML; pass it as the html field.

send-welcome.ts
import { Sendara } from "sendara";
import { render } from "@sendara/react-email";
import { Welcome } from "./emails/Welcome";

const sendara = new Sendara(process.env.SENDARA_API_KEY!);

const html = render(
  <Welcome name="Ada" url="https://acme.com/confirm?t=abc" />,
);

await sendara.emails.send({
  from: "hello@yourdomain.com",
  to: "ada@acme.com",
  subject: "Welcome to Acme",
  html,
});
Want a plain-text part too? render(component, { plainText: true }) returns a text rendering you can pass as text alongside the HTML — most inboxes prefer a multipart message.

Testing with the SDKs

Pass a test key (sk_test_…) and every SDK talks to the sandbox: sends are simulated and never billed, but still drive webhooks. Address the simulator inbox to force an outcome — delivered@, bounced@, or complained@ on any domain.

sandbox.ts
const sandbox = new Sendara(process.env.SENDARA_TEST_KEY!); // sk_test_…

await sandbox.emails.send({
  from: "hello@yourdomain.com",
  to: "bounced@example.com", // simulates a hard bounce + bounced webhook
  subject: "Sandbox check",
  html: "<p>Not really sent.</p>",
});

To send a real email to one of your own verified test recipients (free, capped per day), set testSend — the SDK forwards it as test_send: true:

test-send.ts
await sendara.emails.send({
  from: "hello@yourdomain.com",
  to: "you@yourcompany.com", // must be a verified test recipient
  subject: "UAT — real delivery",
  html: "<p>This one actually arrives.</p>",
  testSend: true,
});
Test-send guards surface as typed errors: RecipientNotVerifiedError(the address isn't a verified test recipient) and TestSendDailyLimitError (the per-address daily cap is hit). See sandbox & test sends for the full flow.
Source, changelogs, and issues live on GitHub. All four packages are versioned together — pin v0.2.0 and upgrade in lockstep.