# Hooks (/docs/v/0.6.1/concepts/hooks)



Hooks are read-only observers of the send pipeline: logs, metrics, traces. They fire for **every adapter attempt**, they cannot change the message, and a throwing hook never breaks a send. To *transform* messages, use [plugin middleware](/docs/v/0.6.1/plugins/writing-plugins) instead — that distinction is the whole design.

```ts title="lib/email.ts"
import { createEmailClient } from "@opencoredev/email-sdk";
import { resend } from "@opencoredev/email-sdk/resend";

const email = createEmailClient({
  adapters: [resend({ apiKey: process.env.RESEND_API_KEY! })],
  retry: { retries: 2 },
  hooks: {
    beforeSend({ provider, attempt, metadata }) {
      console.log("email.attempt", { provider, attempt, route: metadata?.route });
    },
    afterSend({ provider, attempt, response }) {
      console.log("email.sent", { provider, attempt, id: response.id });
    },
    onRetry({ provider, nextAttempt, delayMs, error }) {
      console.warn("email.retry", { provider, nextAttempt, delayMs, error });
    },
    onError({ provider, attempt, error }) {
      console.error("email.failed", { provider, attempt, error });
    },
  },
});

await email.send(message, { metadata: { route: "checkout.receipt" } });
```

The `metadata` here is send-scope: it flows to every hook event for correlation but is never merged into the message — unlike `message.metadata`, which adapters map to the wire.

## The four events [#the-four-events]

Every event carries the same base payload:

<TypeTable
  type="{
  provider: {
    description: &#x22;Routing name of the adapter handling this attempt.&#x22;,
    type: &#x22;string&#x22;,
  },
  message: {
    description: &#x22;The EmailMessage being sent, after middleware transforms.&#x22;,
    type: &#x22;EmailMessage&#x22;,
  },
  attempt: {
    description: &#x22;1-based attempt number, per adapter.&#x22;,
    type: &#x22;number&#x22;,
  },
  metadata: {
    description: &#x22;Send-scope metadata passed in send options.&#x22;,
    type: &#x22;Record<string, unknown>&#x22;,
  },
}"
/>

Each event adds its own fields:

| Event        | Fires                                                                                                         | Extra fields                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `beforeSend` | Before every attempt, on every adapter                                                                        | —                                              |
| `afterSend`  | Once, on the attempt that succeeded                                                                           | `response` (normalized, `provider` always set) |
| `onRetry`    | When the same adapter will be retried, before the backoff sleep                                               | `error`, `nextAttempt`, `delayMs`              |
| `onError`    | Once per adapter, after its final failure — before [fallback](/docs/v/0.6.1/concepts/fallbacks-and-retries) continues | `error`                                        |

So a send with `retries: 2` and one fallback adapter can fire `beforeSend` up to six times, `onRetry` up to four, `onError` up to twice, and `afterSend` at most once.

## Hooks observe, middleware transforms [#hooks-observe-middleware-transforms]

Hooks are deliberately powerless. When you need to *change* a send — add default headers, stamp idempotency keys, rewrite recipients — that is plugin middleware, a separate mechanism with different rules:

|                     | Hooks                                   | Plugin middleware `beforeSend`               |
| ------------------- | --------------------------------------- | -------------------------------------------- |
| Registered via      | Client `hooks` option or `plugin.hooks` | Plugins only (`plugin.middleware`)           |
| Runs                | Per attempt, per adapter                | Once per send, before validation and routing |
| Knows the adapter   | Yes (`provider` on every event)         | No — it runs before the route is resolved    |
| Can change the send | Never                                   | Yes — returns `{ message?, options? }`       |
| If it throws        | Swallowed, send unaffected              | Send fails                                   |

```ts title="A transform belongs in middleware, not a hook"
const brandPlugin = {
  id: "brand",
  middleware: [
    {
      beforeSend({ message }) {
        return { message: { ...message, headers: { "X-App": "acme", ...message.headers } } };
      },
    },
  ],
};
```

[`defaultsPlugin`](/docs/v/0.6.1/plugins/built-in/defaults) is this pattern packaged up. The full middleware surface (`beforeSend`, `afterSend`, `onError`) is in the [plugin API reference](/docs/v/0.6.1/plugins/api).

## Guarantees [#guarantees]

* **Failures are swallowed.** A hook that throws (or rejects) is ignored — observability must never mask a send result. The flip side: never put delivery-critical logic in a hook.
* **Hooks are awaited in order.** Async hooks run sequentially: plugin hooks first, then client hooks. Slow hooks slow down sends, so keep them fast or fire-and-forget internally.
* **Hooks see the final message.** Middleware transforms run first, so `event.message` is what the adapter actually receives.

## Production telemetry and tests [#production-telemetry-and-tests]

Hand-rolled `console.log` hooks leak recipients and bodies into logs sooner or later. Two built-in plugins cover the common cases:

* [`observabilityPlugin`](/docs/v/0.6.1/plugins/built-in/observability) emits `email.sent` / `email.retry` / `email.error` events with a redacted message shape — counts and flags, no bodies or recipients. Use it for production logging, metrics, and tracing.
* [`capturePlugin`](/docs/v/0.6.1/plugins/built-in/capture) records every hook event to an inspectable store — use it to assert send behavior in [tests](/docs/v/0.6.1/guides/test-email-behavior).

## Next [#next]

<Cards>
  <Card title="Observability plugin" href="/docs/v/0.6.1/plugins/built-in/observability" description="Redacted, structured send telemetry for production." />

  <Card title="Writing plugins" href="/docs/v/0.6.1/plugins/writing-plugins" description="Middleware, plugin hooks, adapters, and client extensions." />

  <Card title="Fallbacks and retries" href="/docs/v/0.6.1/concepts/fallbacks-and-retries" description="The pipeline these events narrate: backoff, routes, failures." />
</Cards>
