Send email
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.
/v1/emailsA 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 and metrics.
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\u2019re in</h1><p>Thanks for signing up.</p>",
"text": "You\u2019re in. Thanks for signing up."
}'{
"id": "msg_8f2c1a7e",
"status": "queued"
}from.01 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.
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 fromnoreply@, reply tosupport@).
domains.listVerified() returns every domain you can send from, so you never hard-code a from.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>",
});02 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.
templateId with data for the merge variables. You may not combine templateId with inline html/text. See Templates.03 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.
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",
},
],
});04 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.
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",
},
});05 Request fields
{ email, name? }. The email must belong to a verified domain (or the onboarding domain in test mode).{ email, name? }. At least one.templateId supplies it.html, text, or both.from.{ filename, content, contentType }, where content is base64-encoded bytes.string → string map of custom headers added to the message.string → string map stored on the message for filtering and analytics.templateId.Idempotency-Key header so a retried request after a network blip can't double-send. See Idempotency & retries.