# Plugin API (/docs/v/0.6.1/plugins/api)



Reference for the plugin surface. For patterns and guidance, start with [Writing plugins](/docs/v/0.6.1/plugins/writing-plugins).

## EmailPlugin [#emailplugin]

```ts
type EmailPlugin<TExtension extends object = object> = {
  id: string;
  adapters?: EmailProvider[] | ((ctx: EmailPluginContext) => EmailProvider[]);
  hooks?: EmailHooks;
  middleware?: EmailSendMiddleware[];
  extendClient?: (ctx: EmailPluginContext) => TExtension;
};
```

<TypeTable
  type="{
  id: {
    description: &#x22;Stable plugin id. A duplicate id in the plugins array throws EmailValidationError.&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
  adapters: {
    description:
      &#x22;Providers to register, as an array or a synchronous factory. An async factory or duplicate adapter name throws EmailValidationError.&#x22;,
    type: &#x22;EmailProvider[] | ((ctx) => EmailProvider[])&#x22;,
  },
  hooks: {
    description:
      &#x22;Per-attempt observability callbacks (beforeSend, afterSend, onError, onRetry). Run before client hooks; thrown errors are swallowed.&#x22;,
    type: &#x22;EmailHooks&#x22;,
  },
  middleware: {
    description: &#x22;Send-pipeline middleware. Only plugins can register middleware.&#x22;,
    type: &#x22;EmailSendMiddleware[]&#x22;,
  },
  extendClient: {
    description:
      &#x22;Returns properties merged onto the client. Colliding with an existing client key throws EmailValidationError.&#x22;,
    type: &#x22;(ctx: EmailPluginContext) => TExtension&#x22;,
  },
}"
/>

`createEmailClient` is synchronous: it registers direct `adapters` first, then processes plugins in array order, then applies `extendClient` extensions after the client object is built.

## EmailPluginContext [#emailplugincontext]

Passed to `adapters` factories and `extendClient`.

```ts
type EmailPluginContext = {
  adapters: ReadonlyMap<string, EmailProvider>;
  defaultAdapter: string;
  addAdapter(adapter: EmailProvider): void;
};
```

<TypeTable
  type="{
  adapters: {
    description: &#x22;Live view of registered adapters at the time the plugin runs.&#x22;,
    type: &#x22;ReadonlyMap<string, EmailProvider>&#x22;,
  },
  defaultAdapter: {
    description: &#x22;The requested default adapter name.&#x22;,
    type: &#x22;string&#x22;,
  },
  addAdapter: {
    description: &#x22;Registers an adapter. Duplicate names throw EmailValidationError.&#x22;,
    type: &#x22;(adapter: EmailProvider) => void&#x22;,
  },
}"
/>

If an `adapters` factory both calls `ctx.addAdapter(adapter)` and returns the same adapter object, it registers once.

## EmailSendMiddleware [#emailsendmiddleware]

```ts
type EmailSendMiddleware = {
  beforeSend?: (event: EmailBeforeSendEvent) => MaybePromise<EmailBeforeSendResult | void>;
  afterSend?: (event: EmailAfterSendEvent) => MaybePromise<void>;
  onError?: (event: EmailErrorEvent) => MaybePromise<void>;
};
```

| Callback     | Runs                                                              | Errors        |
| ------------ | ----------------------------------------------------------------- | ------------- |
| `beforeSend` | Once per send, before validation and adapter routing.             | Stop the send |
| `afterSend`  | After a provider returns a normalized response.                   | Swallowed     |
| `onError`    | After an adapter route fails its last retry, before any fallback. | Swallowed     |

### beforeSend [#beforesend]

```ts
type EmailBeforeSendEvent = { message: EmailMessage; options?: SendOptions };
type EmailBeforeSendResult = { message?: EmailMessage; options?: SendOptions };
```

No `provider` field — routing has not happened yet. Returning a result transforms the send: `message` **replaces** the message wholesale, `options` **shallow-merges** over the current send options. Return nothing to observe or to throw for policy. Middleware runs in plugin order, each seeing the previous one's result.

### afterSend and onError [#aftersend-and-onerror]

Both extend the hook event shape:

```ts
type EmailAfterSendEvent = {
  provider: string;
  message: EmailMessage;
  attempt: number;
  metadata?: Record<string, unknown>;
  response: EmailProviderResponse;
};

type EmailErrorEvent = {
  provider: string;
  message: EmailMessage;
  attempt: number;
  metadata?: Record<string, unknown>;
  error: unknown;
};
```

After `onError`, [fallback adapters](/docs/v/0.6.1/concepts/fallbacks-and-retries) may still run — it signals a failed route, not necessarily a failed send.

## Typed client extensions [#typed-client-extensions]

`EmailPlugin<TExtension>` carries the extension type, and `createEmailClient` infers the result from the plugins array:

```ts
// EmailClient<{ capture: EmailCaptureStore }>
const email = createEmailClient({
  adapters: [memoryProvider()],
  plugins: [capturePlugin()],
});
```

Helper types behind the inference:

<TypeTable
  type="{
  &#x22;EmailPluginClientExtension<TPlugin>&#x22;: {
    description: &#x22;Extracts TExtension from a single EmailPlugin<TExtension>.&#x22;,
    type: &#x22;object&#x22;,
  },
  &#x22;EmailPluginClientExtensions<TPlugins>&#x22;: {
    description:
      &#x22;Intersection of all extensions in a readonly plugins tuple — the type argument of the returned EmailClient.&#x22;,
    type: &#x22;object&#x22;,
  },
}"
/>

Multiple extending plugins intersect: the client gains every extension, and a runtime key collision throws `EmailValidationError`.

## Registration errors [#registration-errors]

All thrown by `createEmailClient` as `EmailValidationError`:

| Condition               | Message                                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------ |
| Duplicate plugin id     | `Duplicate email plugin "<id>".`                                                                       |
| Duplicate adapter name  | `Duplicate email adapter "<name>".`                                                                    |
| Async adapters factory  | `Email plugin "<id>" returned async adapters. createEmailClient requires synchronous plugin adapters.` |
| Extension key collision | `Email plugin "<id>" tried to extend the client with reserved key "<key>".`                            |
