Email SDK

Plugin API

Type reference for EmailPlugin, EmailPluginContext, EmailSendMiddleware events, and typed client extensions.

Reference for the plugin surface. For patterns and guidance, start with Writing plugins.

EmailPlugin

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

Prop

Type

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

Passed to adapters factories and extendClient.

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

Prop

Type

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

EmailSendMiddleware

type EmailSendMiddleware = {
  beforeSend?: (event: EmailBeforeSendEvent) => MaybePromise<EmailBeforeSendResult | void>;
  afterSend?: (event: EmailAfterSendEvent) => MaybePromise<void>;
  onError?: (event: EmailErrorEvent) => MaybePromise<void>;
};
CallbackRunsErrors
beforeSendOnce per send, before validation and adapter routing.Stop the send
afterSendAfter a provider returns a normalized response.Swallowed
onErrorAfter an adapter route fails its last retry, before any fallback.Swallowed

beforeSend

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

Both extend the hook event shape:

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 may still run — it signals a failed route, not necessarily a failed send.

Typed client extensions

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

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

Helper types behind the inference:

Prop

Type

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

Registration errors

All thrown by createEmailClient as EmailValidationError:

ConditionMessage
Duplicate plugin idDuplicate email plugin "<id>".
Duplicate adapter nameDuplicate email adapter "<name>".
Async adapters factoryEmail plugin "<id>" returned async adapters. createEmailClient requires synchronous plugin adapters.
Extension key collisionEmail plugin "<id>" tried to extend the client with reserved key "<key>".

On this page