> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> To translate a template, write one locale at a time with `PUT /notifications/{id}/locales/{localeId}`, which merges into the existing translations. `PUT /notifications/{id}/content` and `PUT /notifications/{id}/elements/{elementId}` overwrite translations along with the content, so send each element's `id` and `locales` back, and first remove every `checksum` and any `locales` key that starts with an underscore.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Notification preferences

> How Courier decides at send time whether a user gets a message, and on which channels.

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

Preferences resolve at send time. Courier decides whether each recipient gets the message, and on which channels.

Courier layers the topic's default, any tenant defaults, and the user's own choices. You write no routing logic. Map a template to a subscription topic and Courier enforces every layer.

```mermaid theme={null}
flowchart LR
    A["Send on a topic"] --> B{"Resolved status?"}
    B -->|OPTED_IN| C{"Custom routing?"}
    C -->|Yes| D["User's channels"]
    C -->|No| E["Template's channels"]
    B -->|REQUIRED| F["Deliver"]
    B -->|OPTED_OUT| G["Blocked: UNSUBSCRIBED"]
    B -->|Opt-in, unset| H["Blocked: OPT_IN_REQUIRED"]
```

## How it works

Courier resolves a preference in two stages. First it works out the topic's **effective status** for that user. Then it decides whether that status **delivers or blocks** the send, and on which channels.

### Status precedence

The effective status starts from the topic's default. Each layer below overrides the one above it, and only for fields that were explicitly set:

| Layer | Set in | Overrides |
| - | - | - |
| 1. Topic default | <Doc href="/docs/recipients/preferences/preferences-editor">Preferences Editor</Doc> (each topic's default state) | The baseline for every user |
| 2. Parent tenant default | An ancestor tenant's <Doc href="/docs/tenants/preferences">default preferences</Doc> | The topic default, inherited down the <Doc href="/docs/tenants/context#hierarchy-and-inheritance">tenant hierarchy</Doc> |
| 3. Tenant default | The tenant's own <Doc href="/docs/tenants/preferences">default preferences</Doc> | Its parent's default, for users sent in that tenant's context |
| 4. User preference | The user's own opt-in or opt-out (page or <Doc href="/docs/recipients/preferences/api">API</Doc>) | Every default above it |
| 5. User preference for this tenant | The same call with a <Doc href="/docs/recipients/preferences/api#scope-to-a-tenant">`tenant_id`</Doc> | That user's global preference, in this tenant only |
| Short-circuit | A topic default of `REQUIRED` | Everything below it: user choices are ignored |

Layer 5 is what gives one person separate settings in each organization they belong to. Someone who works with two of your customers can leave alerts on at one and off at the other, from the same account.

A default is one of three states:

* **`OPTED_IN` (on):** users receive the topic unless they opt out.
* **`OPTED_OUT` (off):** users receive nothing until they opt in.
* **`REQUIRED`:** users can't opt out. Courier ignores any user record for the topic and always delivers. Use it for critical messages like security alerts.

Unsubscribe links still render for a `REQUIRED` topic, but opting out has no effect. Leave them off templates mapped to one.

### Delivery and channel routing

Courier then decides delivery:

* **`REQUIRED`** always delivers.
* **`OPTED_OUT`** blocks the send for that topic. The message log shows the reason `UNSUBSCRIBED`.
* An opt-in topic whose resolved status isn't `OPTED_IN` blocks the send with the reason `OPT_IN_REQUIRED`.
* **`OPTED_IN`** proceeds to channel routing.

For an opted-in topic, channels resolve the same way. A user's per-topic channel choice (`custom_routing`) overrides the template's routing when `has_custom_routing` is true and the topic has channel selection enabled in the editor. Without both, the template's channel routing applies.

Courier evaluates preferences to route a send, so a blocked send still counts toward usage. To skip a message before any routing runs, use <Doc href="/docs/send/overrides">send conditions</Doc> instead of a preference.

<Note>
  Where custom routing over the API isn't available, the single-topic write returns `402`, the bulk `PUT` returns `400`, and the bulk `POST` returns `200` with a per-item error. Only `status` is written.
</Note>

## FAQ

<AccordionGroup>
  <Accordion title="Why is my topic opted-in but nothing sends?">
    The topic likely resolves to no deliverable channel. An opted-in topic whose channels are all disabled or unsupported produces an unroutable message with the reason `MISSING_PROVIDER_SUPPORT`, not a delivery. That's different from the preference block reason `UNSUBSCRIBED`, so the log tells you routing stopped it, not preferences. Add an available channel to the topic or template, or configure a provider for the channel you expect.
  </Accordion>

  <Accordion title="Do I need a user_id to read tenant preferences?">
    Preferences are keyed by user, and by tenant only when you pass `tenant_id` alongside the `user_id`. Without a `user_id` you get topic defaults, not a resolved preference. The read API returns the topic-level default even with a `tenant_id`. To see what a tenant sets, read the <Doc href="/docs/tenants/preferences">tenant's default preferences</Doc> instead. Tenant defaults apply at send time, so confirm with a test send rather than a preferences read.
  </Accordion>

  <Accordion title="If a user unsubscribes under one tenant, are they unsubscribed everywhere?">
    Opt-outs are scoped to the tenant context they were set in, so an opt-out under tenant B doesn't affect sends under tenant C. Global and per-tenant preferences are stored separately and don't propagate into one another. Honoring a global opt-out across every tenant is off by default. Contact support to enable it per tenant.
  </Accordion>

  <Accordion title="Can a user opt out of a required topic?">
    A topic whose default is `REQUIRED` always delivers, and the API rejects an attempt to opt a user out of it. Only a tenant or workspace default can change a topic's required status, not a user.
  </Accordion>
</AccordionGroup>
