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 synchronousSendaraclient and anAsyncSendaraclient 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.
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.
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.
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.
*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.
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.
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.
// 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.
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.
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
});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.
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:
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,
});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.v0.2.0 and Go is on v0.2.1. Pin each to its own latest version.