---
title: "Policy checks and hooks"
description: "Block sends with beforeSend, such as to suppressed recipients, and log or meter every attempt with redacted hooks."
---

`beforeSend` blocks messages, for example to people who opted out. `hooks` give you logs and metrics for every send without leaking phone numbers or message text.

## Block sends with beforeSend

`beforeSend` runs once per logical send, after local validation and the idempotency check, before any provider request. Return `{ kind: "allow" }` or `{ kind: "reject", reason }`.

```ts
import { createSmsClient } from "@opencoredev/sms-sdk";
import { twilio } from "@opencoredev/sms-sdk/twilio";
import { db } from "./db";

export const sms = createSmsClient({
  adapters: [
    twilio({
      accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
      authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
      from: "+15550100001",
    }),
  ],
  beforeSend: async ({ message }) => {
    if (await db.suppressions.has(message.to)) {
      return { kind: "reject", reason: "Recipient opted out." };
    }
    return { kind: "allow" };
  },
});
```

A rejection throws `PolicyRejectedError`. Nothing is sent, and fallback never runs. Your `reason` appears only on `error.reason`. It stays out of the error message, `toJSON()`, hooks, and the idempotency store, because it may name a person.

The callback receives `{ id, message, encoding, segments }`: the logical send ID, your `SmsSendInput` as passed, and the segment estimate.

- If `beforeSend` throws, the error propagates out of `send()` unchanged and nothing is sent.
- With an idempotency store, a policy rejection is stored as `rejected`, so the same key can send once the policy allows it.
- It runs inside every `send()`, so keep it fast.

Use it for a suppression list, quiet hours, per-recipient rate limits, or a segment budget. SMS SDK keeps no suppression list for you. [STOP, HELP, and opt-outs](/receiving/stop-help-and-opt-outs) shows how to fill one from webhooks.

## Observe sends with hooks

```ts
import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";

export const sms = createSmsClient({
  adapters: [telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from: "+15550100001" })],
  hooks: {
    onAttempt: ({ id, to, attempt }) => {
      console.log("sms.attempt", { id, to, provider: attempt.provider, outcome: attempt.outcome, ms: attempt.durationMs });
    },
    onAccepted: ({ id, provider, providerId, segments }) => {
      console.log("sms.accepted", { id, provider, providerId, segments });
    },
    onFailure: ({ id, errorCode, retrySafe, attempts }) => {
      console.error("sms.failed", { id, errorCode, retrySafe, attempts: attempts.length });
    },
  },
});
```

This logs one `sms.attempt` line per provider request, then one `sms.accepted` or `sms.failed` line per send.

| Hook | When | Payload beyond the shared fields |
| --- | --- | --- |
| `onAttempt` | After every provider request | `attempt`: a `SendAttempt` |
| `onAccepted` | Once, when a send is accepted | `provider`, `providerId`, `attempts` |
| `onFailure` | Once, when a send ends in an error | `errorCode`, `retrySafe`, `attempts` |

Every payload has `id`, `to` (masked, such as `+1********23`), `encoding`, `segments`, and `idempotencyKey` when set. Hooks never receive the message body, credentials, or full phone numbers.

SMS SDK ignores a hook that throws or rejects, so a broken logger cannot change a send's outcome. `send()` does not await hooks.

`onFailure` does not run for errors thrown before the send starts: local validation errors, idempotency replays and conflicts, and errors thrown by `beforeSend`.

## Logging errors

`SmsError.toJSON()` returns a redacted object with `name`, `code`, `message`, `retrySafe`, `attempts`, and `provider`. Phone numbers in provider messages are masked, and the messages are cut to 200 characters. `cause` is never serialized. If your logger serializes everything, log `error.toJSON()` instead of the raw error.

## Next steps

- [STOP, HELP, and opt-outs](/receiving/stop-help-and-opt-outs)
- [createSmsClient reference](/reference/create-sms-client)
