# Defaults plugin (/docs/v/0.6.1/plugins/built-in/defaults)



`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.

```ts title="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 [#options]

<TypeTable
  type="{
  id: {
    description: &#x22;Plugin id. Override when mounting multiple defaults layers.&#x22;,
    type: &#x22;string&#x22;,
    default: '&#x22;defaults&#x22;',
  },
  headers: {
    description: &#x22;Default message headers, merged under per-message headers.&#x22;,
    type: &#x22;Record<string, string> | EmailHeader[]&#x22;,
  },
  tags: {
    description: &#x22;Default tags, prepended before per-message tags.&#x22;,
    type: &#x22;EmailTag[]&#x22;,
  },
  metadata: {
    description: &#x22;Default message metadata, merged under per-message metadata.&#x22;,
    type: &#x22;Record<string, string | number | boolean | null>&#x22;,
  },
  sendMetadata: {
    description: &#x22;Default send-option metadata (flows to hooks, not the message).&#x22;,
    type: &#x22;Record<string, unknown>&#x22;,
  },
  replyTo: {
    description: &#x22;Default reply-to, used only when the message has none.&#x22;,
    type: &#x22;EmailAddress | EmailAddress[]&#x22;,
  },
  idempotencyKey: {
    description: &#x22;Default idempotency key, used only when the message and send options have none.&#x22;,
    type: &#x22;string&#x22;,
  },
  idempotencyKeyPrefix: {
    description: &#x22;Prefix prepended to the idempotency key unless it already starts with it.&#x22;,
    type: &#x22;string&#x22;,
  },
}"
/>

## Merge semantics [#merge-semantics]

Each option has an exact, predictable rule:

| Option                 | Rule                                                                                                                         |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `headers`              | Merged by name; a message header with the same name **wins** over the default.                                               |
| `metadata`             | Merged by key; message metadata **wins**.                                                                                    |
| `sendMetadata`         | Merged into `options.metadata`; per-send metadata **wins**.                                                                  |
| `tags`                 | Concatenated — defaults first, then message tags. Nothing is replaced.                                                       |
| `replyTo`              | Applied only when the message has no `replyTo` at all.                                                                       |
| `idempotencyKey`       | Applied only when neither the message nor send options carry a key.                                                          |
| `idempotencyKeyPrefix` | Prepended 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.

<Callout type="warn" title="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](/docs/v/0.6.1/adapters/field-support) when adding default tags or
  metadata to a client with fallback routes.
</Callout>

## Stack layers [#stack-layers]

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

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

## Next [#next]

<Cards>
  <Card title="Observability plugin" href="/docs/v/0.6.1/plugins/built-in/observability" description="Redacted sent/retry/error events for logs, metrics, and traces." />

  <Card title="Production send pipeline" href="/docs/v/0.6.1/guides/production-send-pipeline" description="Defaults, retries, idempotency, and observability assembled for a real app." />
</Cards>
