Radondocs

Sending

Everything on an EmailMessage — recipients, HTML and text bodies, reply-to, tags, headers, metadata, and the providerOptions escape hatch.

email.send() takes one EmailMessage and returns a normalized SendResult. This page covers every field you can put on a message. A from (per-message or defaultFrom) is always required — see the from requirement.

const result = await email.send({
  to: "ada@example.com",
  subject: "Your receipt",
  html: "<h1>Thanks!</h1><p>Order #1234 is confirmed.</p>",
  text: "Thanks! Order #1234 is confirmed.",
});

Recipients

Every recipient field — to, cc, bcc — accepts one address or an array, and each address is a bare string or a { email, name } object.

await email.send({
  to: ["ada@example.com", { email: "grace@example.com", name: "Grace Hopper" }],
  cc: "manager@example.com",
  bcc: ["audit@example.com", "archive@example.com"],
  replyTo: { email: "support@acme.com", name: "Acme Support" },
  subject: "Team update",
  text: "…",
});

Not every provider supports cc/bcc/replyTo

Loops, Termii, and Pinpoint don't accept CC/BCC (and Loops/Termii send to exactly one recipient per request). Check provider.capabilities.cc / .bcc / .replyTo, or see the capability matrix.

HTML and text bodies

Provide html, text, or both. Sending both is best practice — clients that can't render HTML fall back to the plain-text part.

await email.send({
  to: "ada@example.com",
  subject: "Welcome",
  html: "<p>Welcome aboard!</p>",
  text: "Welcome aboard!",
});

Both bodies support free {{variable}} interpolation via the variables field — see Templates.

Tags

tags attach categories for analytics and webhook filtering, where the provider supports them (capabilities.tags).

await email.send({
  to: "ada@example.com",
  subject: "Weekly digest",
  html: "…",
  tags: ["digest", "weekly"],
});

Tag handling varies by provider

Radon maps tags to each provider's native concept: Resend tag objects, SendGrid categories, SES EmailTags. Some providers only accept one tag — Postmark, Mailjet, and SparkPost use tags[0] (as Tag, CustomID, and campaign_id respectively). Providers without tag support (SMTP, Loops, Termii, SMTP2GO, Pinpoint, ZeptoMail) ignore the field.

Custom headers

headers adds arbitrary MIME headers, where the provider supports them.

await email.send({
  to: "ada@example.com",
  subject: "Reply in this thread",
  text: "…",
  headers: {
    "X-Entity-Ref-ID": "order-1234",
    "In-Reply-To": "<original-message-id@acme.com>",
  },
});

Metadata

metadata is arbitrary key/value data echoed back where the provider supports it (notably Postmark's Metadata). It's ignored by providers that have no equivalent — use it for correlation, not as a guaranteed round-trip.

await email.send({
  to: "ada@example.com",
  subject: "Your order",
  text: "…",
  metadata: { orderId: "1234", userId: "u_abc" },
});

Scheduling with sendAt

Pass a Date to schedule delivery. Supported on Resend, SendGrid, Mailgun, Brevo, MailerSend, and SparkPost — the result comes back with status: "scheduled". See Attachments & scheduling for the full list and caveats.

await email.send({
  to: "ada@example.com",
  subject: "Happy New Year!",
  text: "…",
  sendAt: new Date("2027-01-01T00:00:00Z"),
});

providerOptions — the escape hatch

Anything the normalized message doesn't model goes in providerOptions. The adapter merges it into the outgoing request verbatim. This is how you reach a provider's native features — a SendGrid dynamic template, a Mailgun tracking flag — without Radon reinventing them.

// SendGrid dynamic template, passed straight through:
await email.send({
  to: "ada@example.com",
  providerOptions: { templateId: "d-abc123", dynamicTemplateData: { name: "Ada" } },
}, { provider: "sendgrid" });

providerOptions ties a call to one provider

Fields in providerOptions are provider-specific and merged last, so they can override normalized fields. Using them couples that send() to one provider — documented, but the opposite of the swap-with-config promise. Reach for it only for features the unified API doesn't cover.

Some providers require a providerOptions value because their API has no arbitrary-body send:

  • Loops needs providerOptions.transactionalId.
  • Termii needs providerOptions.templateId (or .code for OTP).

See Providers for each one's requirements.

The full message shape

toAddress | Address[]required

Recipient(s).

fromAddress

Sender. Overrides the client's defaultFrom. One of the two is required.

ccAddress | Address[]

CC recipient(s), where supported.

bccAddress | Address[]

BCC recipient(s), where supported.

replyToAddress

Reply-To address, where supported.

subjectstring

Subject line. Optional only when a managed template supplies it.

htmlstring

HTML body. At least one of html / text / template should be present.

textstring

Plain-text body / fallback.

variablesRecord<string, unknown>

Values for free {{var}} interpolation into subject/html/text.

templateTemplateRefPro

A managed template { id, variables } to render for this send.

attachmentsAttachment[]

Files to attach — see Attachments.

tagsstring[]

Tags / categories, where supported.

headersRecord<string, string>

Extra MIME headers, where supported.

metadataRecord<string, unknown>

Arbitrary data echoed back where supported.

sendAtDate

Schedule the send for a future time, where supported.

providerOptionsRecord<string, unknown>

Escape hatch for provider-specific request fields. Merged into the request.

Next steps

On this page