Skip to main content

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.

The official SDK is published to npm as drin. It’s a thin, typed wrapper over the REST API with built-in retries, cursor pagination, typed errors, and webhook signature verification.
Requires Node.js 20+ and (for types) TypeScript 5.6+.

Install

npm install drin

Create a client

import { DrinClient } from "drin";

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 (0 = off)
  // maxRetries: 2,                    // transient-failure retries
});
The client exposes one resource per area of the API: emails · domains · inboxes · threads · inbound · webhooks · suppressions · apiKeys · metrics · templates · contacts

Send

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",
});
Pick a verified sending domain in code with the domains.listVerified() convenience:
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>",
});

Paginate

Every list endpoint returns { data, nextCursor }. Use .paginate() for an async iterator that walks every page for you:
for await (const message of drin.emails.paginate({ status: "bounced" })) {
  console.log(message.to, message.subject);
}

Receive & reply

// One-call threaded reply — subject, To, and threading headers are set for you.
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 — pass the raw request body and the Drin-Signature header. It throws on any mismatch.
import { DrinWebhookVerificationError } from "drin";

app.post("/webhooks/drin", async (req) => {
  try {
    const event = drin.webhooks.verify(
      req.rawBody,                       // the exact bytes received
      req.headers["drin-signature"],
      process.env.DRIN_WEBHOOK_SECRET!,
    );
    // event is the typed, verified payload
  } catch (err) {
    if (err instanceof DrinWebhookVerificationError) return res.status(400).end();
    throw err;
  }
});

Handle errors

Every failure is a typed subclass of DrinError — branch on instanceof or err.type. See Errors for the full list.
import { DrinRateLimitError, DrinSuppressedError } from "drin";

React Email

If you author emails with React Email, render and send in one step with the optional helper:
import { sendReactEmail } from "drin/react-email";

await sendReactEmail(drin, {
  from: { email: "hello@acme.com" },
  to: [{ email: "customer@example.com" }],
  subject: "Welcome",
  react: <WelcomeEmail name="Sam" />,
});