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

# Send email

> The core send: from, to, cc, bcc, reply-to, subject, HTML and text bodies, attachments, custom headers, and tags — over cURL or the SDK.

One endpoint sends every transactional message — receipts, magic links, alerts. Give it a from, at least one recipient, and a body. Everything else is optional.

**`POST /v1/emails`**

A send is a single JSON request. The minimum is a `from` address, a `to` array, a `subject`, and a body — either `html`, `text`, or both. A successful call returns `202 Accepted` with a message `id` and a `status` of `queued`; delivery happens asynchronously and surfaces over [webhooks](/webhooks) and [metrics](/metrics).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.drin.run/v1/emails \
    -H "Authorization: Bearer $DRIN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "from": { "email": "hello@acme.com", "name": "Acme" },
      "to": [{ "email": "customer@example.com" }],
      "subject": "Welcome aboard",
      "html": "<h1>You’re in</h1><p>Thanks for signing up.</p>",
      "text": "You’re in. Thanks for signing up."
    }'
  ```

  ```typescript Node.js theme={null}
  import { DrinClient } from "@drin00/sdk";

  const drin = new DrinClient({ apiKey: process.env.DRIN_API_KEY });

  const { id, status } = 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><p>Thanks for signing up.</p>",
    text: "You’re in. Thanks for signing up.",
  });

  console.log(id, status); // "msg_…", "queued"
  ```
</CodeGroup>

```json 202 Accepted theme={null}
{
  "id": "msg_8f2c1a7e",
  "status": "queued"
}
```

<Note>
  **Test mode.** Before you verify a domain, send from the shared onboarding domain — it can only deliver to your own address. To email anyone from your own brand, [verify a domain](/domains) (DKIM + SPF + DMARC, guided), then use it as your `from`.
</Note>

## Recipients

Every address is an object — `{ email }` with an optional `name` for the display name. `to`, `cc`, `bcc`, and `replyTo` are all arrays, so you can pass several at once.

```typescript Multiple recipients theme={null}
await drin.emails.send({
  from: { email: "billing@acme.com", name: "Acme Billing" },
  to: [
    { email: "ada@example.com", name: "Ada Lovelace" },
    { email: "grace@example.com" },
  ],
  cc: [{ email: "records@acme.com" }],
  bcc: [{ email: "archive@acme.com" }],
  replyTo: [{ email: "support@acme.com", name: "Acme Support" }],
  subject: "Your March invoice",
  html: "<p>Invoice attached.</p>",
});
```

* **to** — primary recipients. At least one is required.
* **cc** — carbon copies, visible to everyone.
* **bcc** — blind copies, hidden from other recipients.
* **replyTo** — where replies should go when it differs from `from` (e.g. send from `noreply@`, reply to `support@`).

<Tip>
  **Pick a verified domain in code.** The SDK's `domains.listVerified()` returns every domain you can send from, so you never hard-code a `from`.
</Tip>

```typescript From a verified domain 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>Sent from a domain you control.</p>",
});
```

## Subject and body

The `subject` is required for an inline send. For the body, provide `html`, `text`, or both. Sending both is recommended: clients that can't render HTML — and many spam filters — fall back to the plain-text part, and a good text alternative improves deliverability.

* **html** — the rich body. Inline your CSS; most mail clients strip `<style>` blocks and external stylesheets.
* **text** — the plain-text alternative. If you only send HTML, recipients on text-only clients see nothing.

<Note>
  **Templates instead of inline bodies.** To reuse a body across sends, store it once and send by `templateId` with `data` for the merge variables. You may not combine `templateId` with inline `html`/`text`. See [Templates](/templates).
</Note>

## Attachments

Pass `attachments` as an array. Each item needs a `filename`, the base64-encoded `content`, and a `contentType`. Encode the raw bytes — don't wrap them in a data URL.

<CodeGroup>
  ```typescript Node.js theme={null}
  import { readFileSync } from "node:fs";

  await drin.emails.send({
    from: { email: "billing@acme.com" },
    to: [{ email: "customer@example.com" }],
    subject: "Your receipt",
    html: "<p>Your receipt is attached.</p>",
    attachments: [
      {
        filename: "receipt.pdf",
        content: readFileSync("./receipt.pdf").toString("base64"),
        contentType: "application/pdf",
      },
    ],
  });
  ```

  ```bash cURL theme={null}
  curl https://api.drin.run/v1/emails \
    -H "Authorization: Bearer $DRIN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "from": { "email": "billing@acme.com" },
      "to": [{ "email": "customer@example.com" }],
      "subject": "Your receipt",
      "html": "<p>Your receipt is attached.</p>",
      "attachments": [
        {
          "filename": "receipt.pdf",
          "content": "JVBERi0xLjcKJ… (base64 bytes)",
          "contentType": "application/pdf"
        }
      ]
    }'
  ```
</CodeGroup>

<Warning>
  **Keep attachments small.** The whole request — including base64 attachments, which inflate the raw bytes by \~33% — must fit the API's body limit. For large or shared files, send a link rather than the bytes.
</Warning>

## Custom headers and tags

`headers` is a flat `string → string` map merged into the outgoing message — useful for `List-Unsubscribe`, your own reference IDs, or any header your downstream systems expect. `tags` is a separate `string → string` map stored on the message for filtering and analytics; tags travel on the delivery events you receive over webhooks, but are never added to the email itself.

```typescript Headers + tags theme={null}
await drin.emails.send({
  from: { email: "hello@acme.com" },
  to: [{ email: "customer@example.com" }],
  subject: "Password reset",
  html: "<p>Reset link inside.</p>",
  headers: {
    "X-Entity-Ref-ID": "reset-9a3f",
    "List-Unsubscribe": "<https://acme.com/unsub?u=42>",
  },
  tags: {
    category: "transactional",
    template: "password-reset",
  },
});
```

## Request fields

<ParamField body="from" type="object" required>
  The sender. `{ email, name? }`. The `email` must belong to a verified domain (or the onboarding domain in test mode).
</ParamField>

<ParamField body="to" type="object[]" required>
  Primary recipients, `{ email, name? }`. At least one.
</ParamField>

<ParamField body="subject" type="string" required>
  The subject line. Optional only when `templateId` supplies it.
</ParamField>

<ParamField body="html" type="string">
  The HTML body. Provide `html`, `text`, or both.
</ParamField>

<ParamField body="text" type="string">
  The plain-text body / alternative part.
</ParamField>

<ParamField body="cc" type="object[]">
  Carbon-copy recipients.
</ParamField>

<ParamField body="bcc" type="object[]">
  Blind-carbon-copy recipients.
</ParamField>

<ParamField body="replyTo" type="object[]">
  Where replies are routed when different from `from`.
</ParamField>

<ParamField body="attachments" type="object[]">
  Files to attach. Each is `{ filename, content, contentType }`, where `content` is base64-encoded bytes.
</ParamField>

<ParamField body="headers" type="object">
  A `string → string` map of custom headers added to the message.
</ParamField>

<ParamField body="tags" type="object">
  A `string → string` map stored on the message for filtering and analytics.
</ParamField>

<ParamField body="templateId" type="string">
  Send a stored template by id or slug instead of inline `html`/`text`. See [Templates](/templates).
</ParamField>

<ParamField body="data" type="object">
  Merge variables for `templateId`.
</ParamField>

<ParamField body="scheduledAt" type="string">
  ISO-8601 timestamp to send the message in the future. See [Batch & scheduled](/sending/batch).
</ParamField>

<Tip>
  **Make sends safe to retry.** Pass an `Idempotency-Key` header so a retried request after a network blip can't double-send. See [Idempotency & retries](/idempotency).
</Tip>

## Next

<CardGroup cols={2}>
  <Card title="Batch & scheduled" icon="bolt" href="/sending/batch">
    Send up to 100 in one request, or schedule a send for later.
  </Card>

  <Card title="Templates" icon="layer-group" href="/templates">
    Store reusable HTML with `{{merge}}` variables.
  </Card>

  <Card title="Idempotency" icon="shield" href="/idempotency">
    Retry POSTs safely with an idempotency key.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/emails/send">
    The full `POST /v1/emails` contract.
  </Card>
</CardGroup>
