SendpositoryDocs
Guides

Node.js SDK

The official client - typed errors, automatic retries, webhook verification.

Updated

npm install sendpository

Or 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.

FailureReads (get, list) and emails.sendEvery other call
Rate limit (429) - refused before anything ranRetried, after exactly Retry-AfterRetried, after exactly Retry-After
5xx, timeout, network error, unreadable response - may have happenedRetriedThrown, not retried
Any other 4xx - will fail the same way againThrownThrown

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,
});

On this page