> ## 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`, the default import of the v7 Node SDK. 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.
> 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 cannot opt out. Courier ignores any user record for the topic and always delivers. Use it for critical messages like security alerts.

### 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 is not `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`, applied when `has_custom_routing` is true) overrides the template's routing. Without custom routing, the template's channel routing applies.

<Note>
  Where custom routing over the API is not 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>

## Limits & behavior

### The read API returns the topic default, not the tenant default

Reading a user's preferences returns the **workspace topic default** in `default_status`, even when you pass a `tenant_id`. Courier applies tenant default preferences in the send pipeline, not on the read endpoint. So <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences" /> with a `tenant_id` reflects the topic-level default and the user's own overrides. It never shows a tenant's `default_preferences` as the default. To see what a tenant sets, read the <Doc href="/docs/tenants/preferences">tenant's default preferences</Doc>. To see what actually sends, trust the resolution order above.

### user\_id is required for tenant-context lookups

A preferences read is keyed by the user, and by the tenant only when you supply one. The record key is `{tenant_id}#{user_id}` when `tenant_id` is present, and `{user_id}` otherwise. Omitting `user_id` returns the topic defaults, not a resolved preference, because there is no user to resolve against. Always pass `user_id`. Pass `tenant_id` for a tenant-scoped result.

### Opt-outs are scoped per tenant

A user's opt-out applies to the tenant context it was set in. If a user opts out under tenant B, a send for the same topic under tenant C is unaffected. The two contexts store separate records. Global preferences (set with no tenant) and per-tenant preferences are independent. Setting a global preference does not propagate into a tenant context. (Honoring a global opt-out across tenants is off by default and enabled per tenant by Courier, contact support if you need it.)

### An opted-in topic with no deliverable channel is unroutable, not dropped

A topic can resolve to `OPTED_IN` and still select no valid channel or provider, for example a default-on topic whose only channel is disabled. The message is **unroutable**, not silently dropped. The log shows an undeliverable reason of `MISSING_PROVIDER_SUPPORT`, distinct from the preference block `UNSUBSCRIBED`. That tells you routing stopped the message, not a preference. Add a fallback channel or fix the provider config.

### Usage still counts for opted-out sends

Courier evaluates the recipient's preferences to route a send, so an opted-out send still counts toward usage even though it is blocked. To skip a message before any routing runs, use <Doc href="/docs/send/overrides">send conditions</Doc> instead of a preference.

## Troubleshooting

* **A tenant default is not taking effect.** Confirm you send with the tenant context (`tenant_id`). Check that the user has no per-tenant override, which wins over a tenant default. The read API shows the topic default, not the tenant default, so verify with a test send rather than a preferences read.
* **Custom routing is ignored.** Channel choices apply only when `has_custom_routing` is true on the user's record and the topic has channel selection enabled in the editor. Without both, the template's routing applies.
* **A required topic still shows an unsubscribe link.** Unsubscribe links render for any topic, but opting out of a `REQUIRED` topic has no effect. Leave unsubscribe links off templates mapped to required topics.

## 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. 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`. Tenant defaults apply at send time.
  </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 does not affect sends under tenant C. Global and per-tenant preferences are stored separately and do not propagate into one another.
  </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>
