Email SDK
Adapters

SDK field support

See which EmailMessage fields Email SDK maps for each adapter.

Email SDK keeps one message shape, but email APIs do not all expose the same features. This page documents what each Email SDK adapter maps today. Stable adapters either map a field or reject it with a validation error. They should not silently drop message data.

How to read this page

  • Yes means the adapter maps that EmailMessage field.
  • No means the adapter rejects that field before calling the provider.
  • Values means Email SDK sends each tag's value to the provider.
  • One tag means only one tag can be represented by that provider API. Sending more than one tag fails before the provider request.

Choose routes by message shape

Fallback routes are only safe when the backup adapter can represent the same message fields that matter to your app. Choose primary and backup routes from the fields your production messages use, not just from provider popularity.

NeedStart with
Most complete API field mappingPostmark, SendGrid, AWS SES, Mailgun, Brevo
Resend-style DX with attachmentsResend
Cheap or self-managed deliverySMTP
Product/event emailIterable, Sequenzy, Loops, Plunk
Provider fallback for productionResend plus SMTP, or one primary API plus Postmark
Attachment-heavy transactionalResend, Postmark, SendGrid, Mailgun, Unosend, MailerSend, Mailtrap, Sequenzy

If you are unsure, start with Resend for the first send, then choose fallback routes from this table based on the exact fields your app sends. Do not add a fallback adapter until it can represent the same message shape as your primary route.

Fallback compatibility checklist

  • Does the backup adapter support every EmailMessage field your message uses?
  • Does the backup preserve attachments when receipts, exports, or files matter?
  • Does the backup preserve metadata or tags if your app uses them for provider dashboards, analytics, or routing?
  • Does the backup support reply-to and headers if support workflows depend on them?
  • Has the backup provider account been live-verified in the target environment?

Fallback examples

Use a narrow fallback only for a narrow message shape. This works because both Resend and SMTP can represent the fields used here.

import { createEmailClient } from "@opencoredev/email-sdk";
import { resend } from "@opencoredev/email-sdk/resend";
import { smtp } from "@opencoredev/email-sdk/smtp";

const email = createEmailClient({
  adapters: [
    resend({ apiKey: process.env.RESEND_API_KEY! }),
    smtp({
      host: process.env.SMTP_HOST!,
      auth: {
        user: process.env.SMTP_USER!,
        pass: process.env.SMTP_PASS!,
      },
    }),
  ],
  defaultAdapter: "resend",
  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",
  },
});

Do not use SMTP as a fallback for a message that depends on provider-only fields. This fails before a provider request instead of dropping data.

// This client has SMTP as a fallback, but SMTP cannot send metadata or tags.
await email.send({
  from: "Acme <hello@acme.com>",
  to: "user@example.com",
  subject: "Receipt",
  html: "<p>Thanks for your order.</p>",
  metadata: {
    orderId: "ord_123",
  },
  tags: [{ name: "type", value: "receipt" }],
});

API adapters

These adapters cover most transactional email fields. They are the best fit when your app needs CC, BCC, reply-to, custom headers, tags, metadata, or attachments.

AdapterCCBCCReply-ToHeadersMetadataTagsAttachments
ResendYesYesYesYesNoYesYes
PostmarkYesYesYesYesYesOne tagYes
SendGridYesYesYesYesYesValuesYes
CloudflareYesYesYesYesNoNoYes
UnosendYesYesYesYesNoValuesYes
AWS SESYesYesYesYesNoYesYes
MailgunYesYesYesYesYesValuesYes
MailerSendYesYesYesYesNoValuesYes
BrevoYesYesYesYesYesValuesYes
Mailchimp TransactionalYesYesNoYesYesValuesYes
MailtrapYesYesYesYesYesOne tagYes

Narrow adapters

These adapters are useful, but their public APIs do not match the full EmailMessage shape. Email SDK keeps them stable by rejecting fields it cannot represent.

AdapterCCBCCReply-ToHeadersMetadataTagsAttachments
SparkPostNoNoYesYesYesValuesYes
IterableNoNoNoNoYesNoNo
LoopsNoNoNoNoYesNoYes
SequenzyNoNoYesNoYesNoYes
PlunkNoNoYesYesYesNoYes
ScalewayYesYesYesYesNoNoYes
ZeptoMailYesYesYesNoNoNoYes
MailPaceYesYesYesNoNoNoNo

SMTP transport

SMTP is built in and does not use Nodemailer. It maps address fields and headers directly to the SMTP message, but it does not map provider-only concepts like tags or metadata.

AdapterCCBCCReply-ToHeadersMetadataTagsAttachments
SMTPYesYesYesYesNoNoNo

Validation behavior

Unsupported fields fail fast. For example, Resend rejects metadata, Mailchimp Transactional rejects replyTo, and SMTP rejects attachments. Fail-fast validation protects fallback routes from silent data loss: if a field matters, you find out before the provider request.

Loops attachments map to the provider's transactional attachments payload. Loops documents that transactional attachments require support enablement on the account, so accounts without that enablement should omit attachments.

Sequenzy metadata maps to transactional variables. Reserved metadata keys map to provider fields: sequenzySlug, sequenzyPreview, and subscriberExternalId or sequenzySubscriberExternalId. Generic metadata keys like slug and preview remain variables.

Sequenzy has one provider body field. If both html and text are present, Email SDK sends the HTML body to Sequenzy and keeps the text body for other routes.

Iterable target sends map metadata to Iterable webhook metadata and core message fields into template dataFields. Iterable does not preserve CC, BCC, reply-to, headers, tags, or attachments through this adapter.

The SDK's local checks are not a live deliverability guarantee. Provider accounts still need verified senders or domains, the correct region, API scopes, and any sandbox or recipient allow-list configuration required by that service.

Attachment rules

Attachment content is treated as raw content by default and encoded to Base64 for API adapters that require Base64.

attachments: [
  {
    filename: "receipt.txt",
    content: "Thanks for your order.",
    contentType: "text/plain",
  },
];

If you already have Base64 content, mark it:

attachments: [
  {
    filename: "receipt.pdf",
    content: base64Pdf,
    contentEncoding: "base64",
    contentType: "application/pdf",
  },
];

Adapters that support attachments can also read a local path:

attachments: [
  {
    filename: "receipt.pdf",
    path: "./receipt.pdf",
    contentType: "application/pdf",
  },
];

On this page