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.codefor OTP).
See Providers for each one's requirements.
The full message shape
toAddress | Address[]requiredRecipient(s).
fromAddressSender. 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.
replyToAddressReply-To address, where supported.
subjectstringSubject line. Optional only when a managed template supplies it.
htmlstringHTML body. At least one of html / text / template should be present.
textstringPlain-text body / fallback.
variablesRecord<string, unknown>Values for free {{var}} interpolation into subject/html/text.
templateTemplateRefProA 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.
sendAtDateSchedule the send for a future time, where supported.
providerOptionsRecord<string, unknown>Escape hatch for provider-specific request fields. Merged into the request.
Next steps
Core concepts
The mental model behind Radon Email — providers, the from requirement, capabilities, templates, webhooks, and how swapping providers actually works.
Templates
Two template layers in Radon Email — free provider-agnostic {{variable}} interpolation, and Pro managed templates with a pluggable store.