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 synchronousSendaraclient and anAsyncSendaraclient 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.
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.
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.
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) — exposesretryAfterin 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.
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.
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.
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.
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,
});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.
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:
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,
});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.v0.2.0 and upgrade in lockstep.