Node.js SDK
The official client - typed errors, automatic retries, webhook verification.
Updated
npm install sendpositoryOr let one command do the setup: npx sendpository@latest init connects your
account in the browser, saves the key to your env file and installs the SDK
and the AI coding agent skill.
No dependencies, about 7 kB packed, and works in Node 18+, Bun, Deno and edge runtimes. Every other language talks to the API directly.
Sending
import { Sendpository } from "sendpository";
const sendpository = new Sendpository(process.env.SENDPOSITORY_API_KEY);
const { id } = await sendpository.emails.send({
from: "Acme <hello@mail.yourdomain.com>",
to: ["customer@example.com"],
subject: "Your code is 481920",
html: "<p>It expires in 10 minutes.</p>",
});The key is read from SENDPOSITORY_API_KEY when you don't pass one.
Keys belong on your server
Anyone who can read a key can send as you. Never ship one to a browser or mobile app - call your own backend, and have that call us.
Retries that can't send twice
await sendpository.emails.send(
{ from, to: [user.email], subject: "Your code", html },
{ idempotencyKey: `otp-${user.id}-${session.id}` },
);Replaying the same key returns the original message id rather than sending again. Derive it from the event, not the clock - see Idempotency.
Retries are automatic - twice by default, configurable with
new Sendpository(key, { maxRetries: 5 }) - and follow one rule: never
repeat something that might already have happened, unless repeating it is
harmless.
| Failure | Reads (get, list) and emails.send | Every other call |
|---|---|---|
| Rate limit (429) - refused before anything ran | Retried, after exactly Retry-After | Retried, after exactly Retry-After |
| 5xx, timeout, network error, unreadable response - may have happened | Retried | Thrown, not retried |
| Any other 4xx - will fail the same way again | Thrown | Thrown |
emails.send() is safe to retry because it always carries an idempotency key
(one is generated if you don't pass one), so a repeat returns the original id
instead of sending twice. Creating keys, webhooks and domains, cancelling,
deleting, and emails.sendBatch() have no such key: after an ambiguous
failure the SDK throws rather than risk a second key, a second webhook or a
second batch of email. Check the current state (for a batch, with
emails.list()) before trying again.
Errors
import { SendpositoryError } from "sendpository";
try {
await sendpository.emails.send({ /* … */ });
} catch (err) {
if (!(err instanceof SendpositoryError)) throw err;
if (err.isSuppressed) return; // bounced or complained - not a failure
if (err.isRetryable) return enqueue(); // transient
throw err;
}err.type is one of the stable error types. Branch on it, never
on the message.
Verifying webhooks
import { verifyWebhook } from "sendpository";
export async function POST(req: Request) {
try {
const event = verifyWebhook({
body: await req.text(),
signature: req.headers.get("sendpository-signature") ?? "",
secret: process.env.SENDPOSITORY_WEBHOOK_SECRET!,
});
if (event.type === "email.bounced") await markUndeliverable(event.data.to[0]);
return new Response("ok");
} catch {
return new Response("Invalid signature", { status: 401 });
}
}Pass the raw body
Parsing the JSON and re-serialising it changes the bytes, and the signature won't match. Comparison is constant-time, and the timestamp inside the signature prevents replay.
The rest
await sendpository.emails.sendBatch([msg1, msg2]); // up to 100
await sendpository.emails.get(id); // status + timeline
await sendpository.emails.cancel(id); // scheduled only
await sendpository.domains.create("mail.yourdomain.com");
await sendpository.domains.verify(domainId);
// These need a full-access key
await sendpository.apiKeys.create({ name: "production", permission: "sending" });
await sendpository.webhooks.create({ url: "https://api.yourdomain.com/hooks/email" });
await sendpository.logs.list({ status: "4xx" });
// Receiving (full-access key) - needs sendpository 0.3.0 or later
await sendpository.inbound.addresses.create({ domain_id: domainId, local_part: "support" });
const { data } = await sendpository.inbound.list({ status: "received" });
const message = await sendpository.inbound.get(data[0].id);
if (message.sender_verdict !== "verified") {
// Anyone can write any From line - see /docs/guides/receiving
}Options
new Sendpository(apiKey, {
baseUrl: "https://api.sendpository.com/v1", // self-hosted installs
timeout: 30_000,
maxRetries: 2,
fetch: customFetch,
});