> ## Documentation Index
> Fetch the complete documentation index at: https://docs.drin.run/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> The official drin client — typed, with retries, cursor pagination, typed errors, and local webhook verification.

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](/api-reference/overview) you can do here with types and autocomplete.

<Info>
  **Requirements.** Node.js 20+ and, for types, TypeScript 5.6+. The SDK ships ESM with bundled type declarations — no extra `@types` package needed.
</Info>

## Install

```bash theme={null}
npm install @drin00/sdk
```

## 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](/authentication)).

```typescript theme={null}
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`

## Send

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

```typescript theme={null}
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 theme={null}
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>",
});
```

<Note>
  **Idempotent sends.** Pass an idempotency key as the second argument — `drin.emails.send(body, { idempotencyKey: "order-42" })` — to make a retry safe. See [Idempotency & retries](/idempotency).
</Note>

## 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 theme={null}
// 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 theme={null}
// 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`.

## Receive & reply

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

```typescript theme={null}
// 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>",
});
```

## 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 theme={null}
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;
  }
});
```

<Warning>
  **Use the raw body.** Most 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.
</Warning>

## 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 theme={null}
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](/errors) for the full type table.

## React Email helper

If you author emails with [React Email](https://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 theme={null}
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" />,
});
```

<Note>
  **Optional peer dependency.** The 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.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="API reference" icon="code" href="/api-reference/overview">
    Every endpoint, parameter, and response shape the SDK wraps.
  </Card>

  <Card title="Other languages" icon="code" href="/sdk/rest">
    No TypeScript? Call the same REST API from any stack.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Project-scoped vs account-wide keys, and the `sender` option.
  </Card>

  <Card title="Webhooks" icon="bell" href="/webhooks">
    Subscribe to delivery, bounce, complaint, open, and click events.
  </Card>
</CardGroup>
