Docs
guides · typescript SDK

TypeScript SDK

The official drin client is a thin, fully typed wrapper over the REST API — with built-in retries, cursor pagination, typed errors, and local webhook signature verification.

The SDK is published to npm as @drin00/sdk. It mirrors the wire contract one-to-one, so anything you can do over REST you can do here with types and autocomplete.

RequirementsNode.js 20+ and, for types, TypeScript 5.6+. The SDK ships ESM with bundled type declarations — no extra @types package needed.

01 Install

Shell
npm install @drin00/sdk

02 Create a client

Construct one client and reuse it. The only required option is apiKey; pass sender only when you authenticate with an account-wide key (see Authentication).

TypeScript
import { DrinClient } from "@drin00/sdk";

const drin = new DrinClient({
  apiKey: process.env.DRIN_API_KEY!,   // required
  sender: "my-project",                // only for account-wide keys
  // baseUrl: "https://api.drin.run",  // default
  // timeoutMs: 30_000,                // per-request timeout (ms)
  // maxRetries: 2,                    // transient-failure retries
});

The client exposes one resource per area of the API. Each has typed create / get / list / delete methods as appropriate:

  • emails · domains · inboxes · threads · inbound · webhooks
  • suppressions · contacts · templates · apiKeys · metrics

03 Send

emails.send() resolves to { id } — the message id you can use to retrieve status or reply later.

TypeScript
const { id } = await drin.emails.send({
  from: { email: "hello@acme.com", name: "Acme" },
  to: [{ email: "customer@example.com" }],
  subject: "Welcome aboard",
  html: "<h1>You're in</h1>",
  text: "You're in",
});

To pick a verified sending domain in code rather than hard-coding it, use the domains.listVerified() convenience — it auto-pages and returns only the domains a from address may use:

TypeScript
const [domain] = await drin.domains.listVerified();

await drin.emails.send({
  from: { email: `hello@${domain.domain}` },
  to: [{ email: "customer@example.com" }],
  subject: "Hi",
  html: "<p>…</p>",
});
Idempotent sendsPass an idempotency key as the second argument — drin.emails.send(body, { idempotencyKey: "order-42" }) — to make a retry safe. See Idempotency & retries.

04 Paginate

Every list endpoint returns { data, nextCursor }. The easiest way to walk all of it is .paginate(), an async iterator that fetches each page lazily and stops when nextCursor is null — re-using whatever filter you passed:

TypeScript
// Walk every bounced message across all pages — no cursor bookkeeping.
for await (const message of drin.emails.paginate({ status: "bounced" })) {
  console.log(message.to, message.subject);
}

When you want to control paging yourself — for cursor-based UIs, say — call .list() and thread the cursor:

TypeScript
// Or take one page at a time and thread the cursor yourself.
const page = await drin.emails.list({ limit: 50 });
console.log(page.data.length, "messages");
if (page.nextCursor) {
  const next = await drin.emails.list({ cursor: page.nextCursor });
}

paginate() is available on emails, domains, threads, contacts, suppressions, templates, webhooks, and apiKeys.

05 Receive & reply

Inbound mail lands on an inbox and is joined into a thread alongside your outbound messages. Reply in one call — Drin handles the addressing and the threading headers:

TypeScript
// One-call threaded reply. From, To, Subject ("Re: …"), and the
// In-Reply-To / References headers are all set for you, so the
// recipient's mail client threads the conversation correctly.
await drin.emails.reply(inboundMessageId, {
  html: "<p>Thanks for reaching out — we're on it.</p>",
});

06 Verify a webhook

webhooks.verify() is a pure, local check — no network call. Pass the raw request body (the exact bytes you received, before any JSON parsing) and the Drin-Signature header. It returns the typed payload on success and throws DrinWebhookVerificationError on any mismatch.

TypeScript
import { DrinWebhookVerificationError } from "@drin00/sdk";

app.post("/webhooks/drin", async (req, res) => {
  try {
    const event = drin.webhooks.verify(
      req.rawBody,                       // the exact bytes received
      req.headers["drin-signature"],     // signature header
      process.env.DRIN_WEBHOOK_SECRET!,  // the endpoint's signing secret
    );
    // `event` is the typed, verified WebhookPayload.
    console.log(event.type, event.data);
    res.status(200).end();
  } catch (err) {
    if (err instanceof DrinWebhookVerificationError) {
      return res.status(400).end();
    }
    throw err;
  }
});
Use the raw bodyMost frameworks parse JSON before your handler runs, which re-serializes the bytes and breaks the signature. Capture the raw body (e.g. express.raw()) and verify that, not the parsed object.

07 Typed errors

Every failure is a subclass of DrinError, so you can branch with instanceof or on err.type. The base carries type, status, requestId, and (when present) param.

TypeScript
import {
  DrinError,
  DrinValidationError,
  DrinRateLimitError,
  DrinSuppressedError,
} from "@drin00/sdk";

try {
  await drin.emails.send({ /* … */ });
} catch (err) {
  if (err instanceof DrinSuppressedError) {
    // every recipient is suppressed — nothing was sent
  } else if (err instanceof DrinRateLimitError) {
    await sleep((err.retryAfter ?? 1) * 1000);
  } else if (err instanceof DrinValidationError) {
    console.error("Bad field:", err.param, err.message);
  } else if (err instanceof DrinError) {
    console.error(err.type, err.status, err.requestId);
  }
}

The SDK auto-retries 429 and 5xx with exponential backoff and full jitter (default 2 retries). A POST is retried only when you supplied an idempotency key, so a send is never duplicated. See Errors for the full type table.

08 React Email helper

If you author emails with React Email, render and send in one step with the optional helper exported from @drin00/sdk/react-email. It generates a plain-text alternative by default for deliverability.

TSX
import { sendReactEmail } from "@drin00/sdk/react-email";
import { WelcomeEmail } from "./emails/Welcome";

await sendReactEmail(drin, {
  from: { email: "hello@acme.com" },
  to: [{ email: "customer@example.com" }],
  subject: "Welcome",
  react: <WelcomeEmail name="Sam" />,
});
Optional peer dependencyThe helper imports @react-email/render lazily. Install it (npm i @react-email/render) or pass an explicit render function — the core SDK never forces a React toolchain on anyone who doesn't need it.

09 Next steps