Reference

SDKs

Official libraries for Node/TypeScript, Python, and Go, plus @sendaramail/react-email for authoring emails as JSX. Every SDK is a thin, typed wrapper over the same HTTP API — with idempotent retries, auto-pagination for message listing, and webhook verification built in. Below: install, configure, and a runnable example for every common operation.

There are four official packages tracking the same API surface (Node, Python, and react-email at v0.2.0; Go at v0.2.1). Pin each package to its own latest version:

  • sendara (Node / TypeScript) — npm install sendara. First-class types (dist/index.d.ts), ESM-only, 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/sendaramail/sendara-go. Idiomatic, context-aware, functional options for config, a typed *sendara.Error, and cursor iterators.
  • @sendaramail/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. Sends are idempotent by default — each SDK attaches a generated idempotency_keywhen you don't pass one, so transparent retries never double-send.

Python reserves from, so the email helper takes from_ (keyword-only). It maps to the from_email the API expects — the sender must be on a verified domain. In Go, params are passed by value (sendara.EmailSendParams{…}), and every call takes a context.Context first.

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 to every client constructor.
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. (Go: WithBaseURL, Python: base_url.)
timeoutnumberOptional
Per-request timeout. Node milliseconds (default 30000), Python/Go seconds (default 30). A timeout is retried like any transient failure. (Go: WithTimeout takes a time.Duration.)
maxRetriesnumberOptional
How many times to retry 429, 5xx, and network failures with exponential backoff and full jitter. Default 2 (Node/Python) / 3 (Go). Set to 0 to disable client-side retries. (Go: WithMaxRetries.)
Retries apply to 429, 5xx, and network failures only, with exponential backoff and full jitter. A 429 honors the Retry-After header. 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.

Node and Python map status codes onto subclasses you can instanceof / except on: AuthenticationError (401), PermissionDeniedError / PermissionError_ (403), ValidationError (400/422), NotFoundError (404), ConflictError (409), RateLimitError (429, exposing retryAfter/retry_after), and ServerError (5xx). All extend SendaraError. Sub-cases within a status (a suppressed recipient vs. an idempotency conflict, both 409) are distinguished by the string code — see the full list on the errors page.

Go takes a different shape: a single *sendara.Error with a Code string field, classifier methods (IsRateLimited(), IsConflict(), IsValidation(), …), and errors.As to unwrap. There are no per-status Go error types.

Common operations

The same task in all three SDKs. Each block is real, runnable code against the current packages — copy the tab for your language.

Send an email

The ergonomic emails.send helper takes a flat object and returns the created message (id, status).

Send a batch

One request, many sends. Each item is processed independently, so partial success is normal — the result preserves request order with per-item success or error. Give every item its own idempotency_key.

Create and send a broadcast

Broadcasts fan a single email out to a saved audience list or an inline recipient set (each with per-recipient template data). Create then send, or use bulkSend to do both at once, then poll get for live delivery stats. See broadcasts.

Send using a saved template

Pass templateId + templateVars instead of an inline body to render a stored template at send time. Use templates.render to preview the resolved payload without sending.

Create a contact and add it to a list

Manage your audience with contacts and lists. Note the namespacing differs: Node nests lists under contacts.lists, Go under Contacts.Lists, and Python exposes a top-level lists resource.

List messages with auto-pagination

List endpoints are cursor-paginated (limit up to 100, an opaque cursor, and a next_cursor in the response). The iterators hide the cursor plumbing and fetch each page lazily as you go.

The names differ per language: Node iterates messages.list(…) directly (and messages.page(…) for a single page), Python uses messages.iter(…) to auto-paginate and messages.list(…) for one page, and Go uses Messages.Iterator(…) with Next(ctx)/Message()/Err(). Ordering is keyset over created_atdescending, so it's stable as new messages arrive. See pagination.

Add and verify a domain

Register a sending domain, publish the returned DNS records, then re-check verification. fully_verified flips true once DKIM, SPF, and DMARC all pass. See domains.

Create and rotate an API key

Key management needs an admin-scoped key or a dashboard session. The plaintext key is returned exactly once on create and rotate — store it immediately.

Verifying webhooks

Each SDK ships a verify helper 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(), Go io.ReadAll(r.Body)) or verification will always fail. Mind the helper shapes: verifyWebhook(rawBody, headers, secret) in Node, webhooks.verify(secret, payload, headers) in Python, and sendara.VerifyWebhookRequest(secret, header, body, tolerance) in Go. Full scheme and retry semantics live on the webhooks page.

Idempotency

Every send carries an idempotency_key. Retrying with the same key returns the original result instead of sending again, so network retries never double-send. The SDKs generate one automatically for emails.send; for batches and broadcasts, set a deterministic key per item so a retried job is a no-op. Reusing a key with a different payload returns 409 idempotency_key_reused.

idempotency.ts
// Pass your own key to make a specific send safely retriable end-to-end.
await sendara.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Your receipt",
  html: "<p>Thanks!</p>",
  idempotencyKey: `receipt-${orderId}`, // same order ⇒ at most one email
});

Authoring emails with React

@sendaramail/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 "@sendaramail/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 is async and resolves to a string of inlined HTML — pass it as the html field. Use renderEmail when you want a multipart message with a plain-text part too.

send-welcome.tsx
import { Sendara } from "sendara";
import { renderEmail } from "@sendaramail/react-email";
import { Welcome } from "./emails/Welcome";

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

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

await sendara.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome to Acme",
  html,
  text, // most inboxes prefer a multipart message
});
Need just one part? render(component) returns the HTML and renderText(component)returns the plain-text rendering — both async. There's a deeper walkthrough, including prebuilt branded templates, in the React Email guide.

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: "[email protected]",
  to: "[email protected]", // 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: "[email protected]",
  to: "[email protected]", // must be a verified test recipient
  subject: "UAT — real delivery",
  html: "<p>This one actually arrives.</p>",
  testSend: true,
});
Test-send guards surface as 409 conflicts you can branch on by code(the address isn't a verified test recipient, or the per-address daily cap is hit). See sandbox & test sends for the full flow.
Source, changelogs, and issues live on GitHub. The packages version independently — Node, Python, and react-email are on v0.2.0 and Go is on v0.2.1. Pin each to its own latest version.