Field support
Which EmailMessage fields each adapter maps — and which it rejects before the request.
Email SDK keeps one message shape, but provider APIs do not expose the same features. Every adapter either maps a field or rejects it with an EmailValidationError before calling the provider — never a silent drop. This page is the authoritative matrix.
Reading the tables: Yes = mapped, No = rejected before the request, Values = each tag's value is sent, One tag = the provider API represents a single tag and a second one fails fast.
API adapters
The best fit when your app needs CC, BCC, reply-to, custom headers, tags, metadata, or attachments.
| Adapter | CC | BCC | Reply-To | Headers | Metadata | Tags | Attachments |
|---|---|---|---|---|---|---|---|
| Resend | Yes | Yes | Yes | Yes | No | Yes | Yes |
| Postmark | Yes | Yes | Yes | Yes | Yes | One tag | Yes |
| SendGrid | Yes | Yes | Yes | Yes | Yes | Values | Yes |
| Cloudflare | Yes | Yes | Yes | Yes | No | No | Yes |
| Unosend | Yes | Yes | Yes | Yes | No | Values | Yes |
| AWS SES | Yes | Yes | Yes | Yes | No | Yes | Yes |
| Mailgun | Yes | Yes | Yes | Yes | Yes | Values | Yes |
| MailerSend | Yes | Yes | Yes | Yes | No | Values | Yes |
| Brevo | Yes | Yes | Yes | Yes | Yes | Values | Yes |
| Mailchimp Transactional | Yes | Yes | No | Yes | Yes | Values | Yes |
| Mailtrap | Yes | Yes | Yes | Yes | Yes | One tag | Yes |
Narrow adapters
Useful services whose public APIs cover less of the EmailMessage shape. The SDK keeps them safe by rejecting what they cannot represent.
| Adapter | CC | BCC | Reply-To | Headers | Metadata | Tags | Attachments |
|---|---|---|---|---|---|---|---|
| SparkPost | No | No | Yes | Yes | Yes | Values | Yes |
| Iterable | No | No | No | No | Yes | No | No |
| Loops | No | No | No | No | Yes | No | Yes |
| Sequenzy | No | No | Yes | No | Yes | No | Yes |
| Plunk | No | No | Yes | Yes | Yes | No | Yes |
| Scaleway | Yes | Yes | Yes | Yes | No | No | Yes |
| ZeptoMail | Yes | Yes | Yes | No | No | No | Yes |
| MailPace | Yes | Yes | Yes | No | No | No | No |
SMTP transport
Built in, no Nodemailer. SMTP maps address fields and headers directly into the message but has no provider-side concepts like tags or metadata.
| Adapter | CC | BCC | Reply-To | Headers | Metadata | Tags | Attachments |
|---|---|---|---|---|---|---|---|
| SMTP | Yes | Yes | Yes | Yes | No | No | No |
Choosing compatible routes
A fallback route is only safe when the backup adapter supports every field your messages actually use. Pick routes from your message shape, not provider popularity:
| Your messages use… | Compatible route examples |
|---|---|
| Addresses, subject, body, headers | Almost anything — Resend + SMTP works |
| Attachments | Resend, Postmark, SendGrid, Mailgun, MailerSend… |
| Metadata for analytics or routing | Postmark, SendGrid, Mailgun, Brevo, Mailtrap |
| Tags and metadata together | SendGrid, Mailgun, Brevo, Mailtrap |
This pair works because both routes can carry every field used:
const email = createEmailClient({
adapters: [resend({ apiKey: process.env.RESEND_API_KEY! }), smtp({ host: process.env.SMTP_HOST! })],
fallback: ["smtp"],
});
await email.send({
from: "Acme <hello@acme.com>",
to: "user@example.com",
replyTo: "support@example.com",
subject: "Password reset",
text: "Use this link to reset your password.",
headers: { "X-Template": "password-reset" },
});The same client with tags or metadata on the message would fail fast on the SMTP route instead of dropping those fields — which is exactly the point.
Before adding a backup route, run through this checklist:
- Does the backup adapter support every
EmailMessagefield your messages use? - Does it preserve attachments when receipts, exports, or files matter?
- Does it preserve
metadataortagsyour app relies on for provider dashboards, analytics, or routing? - Does it support
replyToandheadersif support workflows depend on them? - Has the backup provider account been live-verified (one real smoke send) in the target environment?
Attachment rules
Attachment content is treated as raw data by default and Base64-encoded for APIs that need it:
attachments: [{ filename: "receipt.txt", content: "Thanks for your order.", contentType: "text/plain" }];Already have Base64? Mark it so it is not double-encoded:
attachments: [
{ filename: "receipt.pdf", content: base64Pdf, contentEncoding: "base64", contentType: "application/pdf" },
];Adapters with attachment support can also read from disk via path:
attachments: [{ filename: "receipt.pdf", path: "./receipt.pdf", contentType: "application/pdf" }];Adapter-specific notes
Local checks are not deliverability
These checks stop bad requests before they leave your process. Live delivery still needs a ready provider account: verified senders or domains, the right region, API scopes, and any sandbox or allow-list rules. Finish with one real smoke send per route.
