Email SDK
PluginsBuilt-in

Defaults plugin

Merge org-wide headers, tags, reply-to, and idempotency key prefixes into every send — with message values winning.

defaultsPlugin injects shared values into every message before validation, so org-wide headers, tags, and a reply-to address live in one place instead of every call site. Per-message values always take precedence over the defaults.

lib/email.ts
import { createEmailClient } from "@opencoredev/email-sdk";
import { defaultsPlugin } from "@opencoredev/email-sdk/plugins/defaults";
import { resend } from "@opencoredev/email-sdk/resend";

export const email = createEmailClient({
  adapters: [resend({ apiKey: process.env.RESEND_API_KEY! })],
  plugins: [
    defaultsPlugin({
      headers: { "X-Entity-Ref": "acme-app" },
      tags: [{ name: "env", value: process.env.NODE_ENV! }],
      replyTo: "support@acme.com",
      idempotencyKeyPrefix: "acme:",
    }),
  ],
});

// Every send now carries the header, the env tag, and the reply-to —
// without repeating them in application code:
await email.send({
  from: "Acme <hello@acme.com>",
  to: "user@example.com",
  subject: "Welcome",
  text: "Your account is ready.",
});

Options

Prop

Type

Merge semantics

Each option has an exact, predictable rule:

OptionRule
headersMerged by name; a message header with the same name wins over the default.
metadataMerged by key; message metadata wins.
sendMetadataMerged into options.metadata; per-send metadata wins.
tagsConcatenated — defaults first, then message tags. Nothing is replaced.
replyToApplied only when the message has no replyTo at all.
idempotencyKeyApplied only when neither the message nor send options carry a key.
idempotencyKeyPrefixPrepended to whichever key ends up applying — unless the key already starts with the prefix, so re-runs never double-prefix.

So with the client above, a message that sets headers: { "X-Entity-Ref": "checkout" } sends checkout (message wins), while its tags gain the env tag in front of its own.

Defaults are validated like any other field

Defaults merge before adapter validation. If a default adds a field the selected route cannot represent — tags on SMTP, for example — the send throws an EmailValidationError before any request. Check field support when adding default tags or metadata to a client with fallback routes.

Stack layers

Mount multiple defaults plugins with distinct ids; they apply in array order, so later layers see the merged result of earlier ones:

plugins: [
  defaultsPlugin({ id: "defaults:org", headers: { "X-Org": "acme" } }),
  defaultsPlugin({ id: "defaults:billing", tags: [{ name: "team", value: "billing" }] }),
];

Next

On this page