# Hooks

> Run your own code after each delivery attempt and when the worker pauses an endpoint.

Source: https://good-webhooks.dev/docs/guides/hooks



Hooks are functions the worker calls after it records an outcome. Use them to tell endpoint owners about automatic pauses and to record metrics. Pass them as `hooks` to `createWebhooks()` or `createDelivery()`.

## Tell owners about automatic pauses [#tell-owners-about-automatic-pauses]

The worker [pauses an endpoint](https://good-webhooks.dev/docs/guides/endpoints.md#automatic-pauses) when every attempt to it has failed for five days, or when it answers `410 Gone`. `onEndpointPaused` runs once for each automatic pause, so you can tell the owner, for example by email:

```ts title="webhooks.ts"
export const webhooks = createWebhooks({
  database,
  encryptionKey: process.env.WEBHOOK_ENCRYPTION_KEY!,
  events,
  hooks: {
    // jobs stands for your own queue. Queue the email instead of sending it: the worker waits for the hook.
    onEndpointPaused: ({ scope, endpointId, reason }) =>
      jobs.enqueue('endpoint-paused', { scope, endpointId, reason }),
  },
})
```

`reason` is `'failing'` or `'gone'`. `scope` is the endpoint's [scope](https://good-webhooks.dev/docs/concepts/scopes.md), or `null` without one. With the [Better Auth plugin](https://good-webhooks.dev/docs/better-auth.md), a `'user'` or `'organization'` scope names the owner, so use `scope.id` to find who to email. [Application and custom scopes](https://good-webhooks.dev/docs/better-auth/permissions.md#application-and-custom-scopes) need your own lookup. Pausing with `pause()` doesn't call the hook.

## Record attempt metrics [#record-attempt-metrics]

`onAttempt` runs once for each attempt the worker records:

```ts title="webhooks.ts"
export const webhooks = createWebhooks({
  database,
  encryptionKey: process.env.WEBHOOK_ENCRYPTION_KEY!,
  events,
  hooks: {
    // metrics stands for your own metrics client.
    onAttempt: ({ eventType, outcome, responseStatus, durationMs }) =>
      metrics.record('webhook.attempt', durationMs, { eventType, outcome, status: responseStatus }),
  },
})
```

`outcome` is one of the [attempt outcomes](https://good-webhooks.dev/docs/reference/delivery.md#attempt-outcomes): `'succeeded'`, `'retry'`, `'failed'`, or `'abandoned'`. `responseStatus` is `null` when no response arrived, and `error` then says why, for example `network_error`. The [supervised worker](https://good-webhooks.dev/docs/operations/workers.md#a-supervised-worker-process) logs both hooks as JSON lines.

## How hooks run [#how-hooks-run]

* The worker calls a hook after it commits the outcome. A hook can't change the outcome.
* The worker waits for each hook, so keep hooks fast and queue slow work such as email.
* A hook that throws is reported like an internal failure: to `onError` in `run()`, or in the `AggregateError` that `run()` and `runOnce()` throw. The outcome stays recorded.
* Hooks are best-effort. A crash between the commit and the call skips them, so read `pauseReason` when you need the record.

The [delivery reference](https://good-webhooks.dev/docs/reference/delivery.md#hooks) lists every field.
