# Adapter contract (/docs/v/0.6.1/reference/adapter-contract)



An adapter is a plain object implementing `EmailProvider`: one routing name, one `send` function that maps a normalized [message](/docs/v/0.6.1/reference/message) to one provider API. This page is the type contract; the walkthrough is [Create an adapter](/docs/v/0.6.1/guides/authoring/create-adapter).

## `EmailProvider` [#emailprovider]

```ts
type EmailProvider<TRaw = unknown> = {
  name: string;
  send(message: EmailMessage, context: EmailProviderContext): MaybePromise<EmailProviderResponse>;
  raw?: TRaw;
};
```

<TypeTable
  type="{
  name: {
    description: &#x22;Stable routing name. Used in defaultAdapter, fallback lists, per-send overrides, hooks, and errors.&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
  send: {
    description: &#x22;Maps the message to one provider request and returns a normalized response.&#x22;,
    type: &#x22;(message, context) => MaybePromise<EmailProviderResponse>&#x22;,
    required: true,
  },
  raw: {
    description: &#x22;Optional escape hatch for provider-specific extras, reachable via client.adapter(name).raw.&#x22;,
    type: &#x22;TRaw&#x22;,
  },
}"
/>

```ts
import { EmailProviderError } from "@opencoredev/email-sdk";
import type { EmailProvider } from "@opencoredev/email-sdk";

export const acmeMail: EmailProvider = {
  name: "acme-mail",
  async send(message, context) {
    const response = await fetch("https://api.acme-mail.example/send", {
      method: "POST",
      signal: context.signal,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(toAcmePayload(message)),
    });

    if (!response.ok) {
      throw new EmailProviderError(`acme-mail failed with HTTP ${response.status}.`, {
        provider: "acme-mail",
        status: response.status,
        retryable: response.status === 429 || response.status >= 500,
      });
    }

    const body = await response.json();
    return { provider: "acme-mail", id: body.id, raw: body };
  },
};
```

## `EmailProviderContext` [#emailprovidercontext]

The second argument to `send`. Honor `signal` and `idempotencyKey` whenever the provider can.

<TypeTable
  type="{
  attempt: {
    description: &#x22;1-based attempt number for this adapter within the current send.&#x22;,
    type: &#x22;number&#x22;,
    required: true,
  },
  signal: {
    description: &#x22;Abort signal from send options. Pass it into fetch.&#x22;,
    type: &#x22;AbortSignal&#x22;,
  },
  idempotencyKey: {
    description: &#x22;Resolved key from send options or message.idempotencyKey. Forward it when the provider deduplicates.&#x22;,
    type: &#x22;string&#x22;,
  },
  metadata: {
    description: &#x22;Send-scope metadata from send options. Observability data, not message content.&#x22;,
    type: &#x22;Record<string, unknown>&#x22;,
  },
}"
/>

## Response contract [#response-contract]

Return an [`EmailProviderResponse`](/docs/v/0.6.1/reference/client#response). Set `provider` to the adapter's routing name (the client fills it in if left empty), map the provider's message id into `id`/`messageId`, and keep the untouched body in `raw` for debugging.

## Error contract [#error-contract]

Throw `EmailProviderError` for provider failures and classify retryability so the client's [retry layer](/docs/v/0.6.1/concepts/fallbacks-and-retries) works:

* Set `status` to the HTTP status when there is one.
* Set `retryable: true` for transient failures. The SDK's own adapters use `isRetryableStatus`: `408`, `409`, `425`, `429`, and any `5xx`.
* Put the parsed error body in `details`.

Anything else you throw is normalized by the client via `toProviderError`: plain `Error`s become non-retryable `EmailProviderError`s — except network-style failures (`ECONNRESET`-family codes, `fetch failed`, timeouts), which are marked retryable. `AbortError` is never retried. See the [errors reference](/docs/v/0.6.1/reference/errors) for the full taxonomy.

## Fail fast on unsupported fields [#fail-fast-on-unsupported-fields]

Adapters must reject message fields the provider cannot represent — before any request, never a silent drop. Built-in adapters throw `EmailValidationError` with this exact shape:

```txt
<adapter> does not support these EmailMessage fields: tags, metadata.
```

Do the same in custom adapters: check `cc`, `bcc`, `replyTo`, `headers`, `attachments`, `tags`, and `metadata` against what your provider supports, and throw for the rest. This is what keeps [fallback routes](/docs/v/0.6.1/adapters/field-support#choosing-compatible-routes) honest.

## Internal helpers [#internal-helpers]

The built-in adapters share two internal modules in the SDK repository — useful as reference, or directly if you contribute an adapter upstream (they are not public package exports):

* `src/http.ts` — `jsonProvider({ name, baseUrl, endpoint, headers, buildPayload, parseResponse? })` wraps the whole JSON-API pattern: fetch with `context.signal`, error-body parsing, `EmailProviderError` with `isRetryableStatus`, and response normalization.
* `src/payloads.ts` — address and attachment mapping helpers (`apiAddresses`, `stringAddresses`, `base64Attachments`, `commonHeadersObject`, …) used across the 20 adapters.

Community packages should copy these patterns rather than import them.

## Packaging as a plugin [#packaging-as-a-plugin]

Ship a reusable adapter as an `EmailPlugin` so users mount it with `plugins: [...]`:

```ts
import type { EmailPlugin } from "@opencoredev/email-sdk";

export function acmeMailPlugin(options: AcmeMailOptions): EmailPlugin {
  return {
    id: "acme-mail",
    adapters: [createAcmeMail(options)],
  };
}
```

`adapters` must be synchronous — an array, or a function of the plugin context that returns one. A plugin that returns a Promise throws `EmailValidationError` at client creation, as do duplicate plugin ids and duplicate adapter names.

Export both the plain adapter factory and the plugin from your package root, then follow [Publish a community adapter](/docs/v/0.6.1/guides/authoring/publish-community-adapter) to list it in the [community registry](/docs/v/0.6.1/reference/community-registry).
