> ## 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.

# Subscription topics and sections

> Topics, sections, default states, and custom routing, and the API that reads and writes them.

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 AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Guide = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="guide" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">GUIDE</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 are subscription topics grouped into sections.

Each topic carries a default state and optional per-channel routing. You define the model in the <Doc href="/docs/recipients/preferences/preferences-editor">Preferences Editor</Doc>. You read and write a user's choices through the User Preferences API.

For how these pieces combine into a delivery decision, see <Doc href="/docs/recipients/preferences/overview">How preferences resolve</Doc>.

## The model

A preference is an **override on a topic's default**. A user falls back to the default until they make a choice. Courier then stores an override on that topic.

* **Subscription topic** is a category users control independently, such as marketing or product updates. Each topic has an ID (a TypeID prefixed `pt_`, for example `pt_01j9x8a2c4e6g8k0m2p4r6t8w0`), a default state, and a section. Templates map to a topic, so every send respects the user's choice.
* **Section** groups related topics under a heading and controls which channels users can adjust for the topics inside it. Section IDs are TypeIDs prefixed `ps_`.
* **Default state** is `OPTED_IN`, `OPTED_OUT`, or `REQUIRED`. It seeds every user's status until they override it. `REQUIRED` cannot be overridden. See <Doc href="/docs/recipients/preferences/overview#status-precedence">how these resolve</Doc>.
* **Custom routing** is the set of channels a user picked for a topic: `direct_message`, `email`, `inbox`, `push`, `sms`, `webhook`. Courier stores them in `custom_routing` and applies them only when `has_custom_routing` is true.
* **Topic data** is arbitrary metadata you attach to a topic through the API, useful for filtering topics or driving a custom preference UI. The API accepts it, and there is no console editor for it.

A user's stored override has three parts:

* `status`, either `OPTED_IN` or `OPTED_OUT`. These are the only two values a user can set.
* `custom_routing`, with its `has_custom_routing` flag.
* `topic_id`, the topic the override applies to.

On reads, `status` can also come back as `REQUIRED`. The API falls back to the topic default when the user has no override of their own.

## The API surface

Two API layers touch preferences. The **User Preferences API** reads and writes one user's choices. The **Workspace Preferences API** manages the topic and section catalog that those choices reference.

### User preferences

| Path                                                                                                                                                                                                                                         | Purpose                                                                |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences" />                                                                                     | List a user's preferences.                                             |
| <Endpoint method="PUT" path="/users/{user_id}/preferences" name="Replace user Preferences in bulk" href="/docs/api-reference/user-preferences/replace-user-preferences-in-bulk" />                                                                | Replace a user's entire override set (bulk, atomic).                   |
| <Endpoint method="POST" path="/users/{user_id}/preferences" name="Update user Preferences in bulk" href="/docs/api-reference/user-preferences/update-user-preferences-in-bulk" />                                                                 | Additively update preferences without touching others (bulk, partial). |
| <Endpoint method="GET" path="/users/{user_id}/preferences/{topic_id}" name="Get user Subscription Topic" href="/docs/api-reference/user-preferences/get-user-subscription-topic" />                                                               | Get one topic's preference.                                            |
| <Endpoint method="PUT" path="/users/{user_id}/preferences/{topic_id}" name="Update or create user Preferences for a Subscription Topic" href="/docs/api-reference/user-preferences/update-or-create-user-preferences-for-a-subscription-topic" /> | Create or update one topic's preference.                               |
| <Endpoint method="DELETE" path="/users/{user_id}/preferences/{topic_id}" name="Delete user Subscription Topic" href="/docs/api-reference/user-preferences/delete-user-subscription-topic" />                                                      | Reset one topic to its default.                                        |

Every endpoint accepts an optional `tenant_id` query parameter to scope preferences to one tenant. Both bulk endpoints accept up to **50 topics per request**. No endpoint updates many users at once, so an import across your user base means one bulk call per user. For worked examples, see <Doc href="/docs/recipients/preferences/api">Manage preferences via API</Doc>.

### Reading current state

<Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences" /> returns a row for **every** topic in every section, not only the topics the user has touched. Each row carries the topic's `default_status` plus an effective `status`. The effective `status` is the user's override when one exists, and the topic default otherwise:

```json theme={null}
{
  "paging": {
    "cursor": null,
    "more": false
  },
  "items": [
    {
      "topic_id": "pt_01j9x8a2c4e6g8k0m2p4r6t8w0",
      "topic_name": "Product updates",
      "section_id": "ps_01j9x8a2c4e6g8k0m2p4r6t8w1",
      "section_name": "Notifications",
      "status": "OPTED_IN",
      "default_status": "OPTED_IN",
      "has_custom_routing": true,
      "custom_routing": ["email", "push"]
    }
  ]
}
```

Two things about this response:

* **`status` is effective, not stored.** A topic the user never touched still returns a `status`, copied from `default_status`. A `REQUIRED` topic therefore reads back as `"status": "REQUIRED"`. Branch on `default_status` to know whether the user chose anything.
* **Paging is not implemented.** `more` is always `false` and no cursor is returned, so a single call gives you every topic.

The read endpoint reports the **topic-level** `default_status`, even when you pass a `tenant_id`. Courier applies tenant default preferences when a message sends, and does not surface them here. See <Doc href="/docs/recipients/preferences/overview#the-read-api-returns-the-topic-default-not-the-tenant-default">how preferences resolve</Doc>.

### Topic and section catalog

The Workspace Preferences API manages the topics and sections themselves. List a section's topics with <Endpoint method="GET" path="/preferences/sections/{section_id}/topics" name="List Topics in Section" href="/docs/api-reference/preference-topics/list-topics-in-section" />. Create, retrieve, replace, or archive individual topics and sections under `/preferences/sections`. Most teams define these in the <Doc href="/docs/recipients/preferences/preferences-editor">Preferences Editor</Doc> instead of by hand.

## Digest schedules

A digest collects a topic's messages and delivers them together on a schedule. A busy topic becomes one daily or weekly summary instead of a stream. You configure digests **per subscription topic** in the <Guide href="/docs/guides/build-a-preference-center#configure-the-topics">Preferences Editor</Guide>, where you link a digest template and define the schedule options recipients choose from.

**How a digest sends:** when a recipient is on a digest schedule for a topic, Courier collects the topic's messages instead of delivering each one. It renders the digest template at the scheduled time. Collected messages show a `DIGESTED` status in your logs. Recipients on the **Instant** schedule receive each message immediately, with no batching.

**Schedule options** are the frequencies recipients can pick (up to four per topic): Instant, Daily, Weekly, Every weekday, Multiple days, or Monthly. Each scheduled option carries a time and timezone. The first option in the list is the default.

**Categories** (up to five per topic) separate kinds of items within one digest, each with a retain rule (first, last, highest, or lowest ten). The digest template receives the collected items grouped by category name:

```json theme={null}
{
  "category_name": {
    "count": 25,
    "items": []
  }
}
```

**Trigger empty:** by default Courier skips a scheduled digest that collected nothing. Turn on Trigger empty to send it anyway, useful when your own system supplies the data the digest renders.

Schedules are read-only over REST. You set a schedule in the editor, not through a public endpoint. The API offers <Endpoint method="POST" path="/digests/schedules/{schedule_id}/trigger" name="Release a digest early" href="/docs/api-reference/digests/release-a-digest-early" /> and <Endpoint method="GET" path="/digests/schedules/{schedule_id}/instances" name="List digest instances" href="/docs/api-reference/digests/list-digest-instances" />. For time-based batching inside a workflow, see the <Doc href="/docs/journeys/nodes/digest">Digest node</Doc>.

## Audit trail

Courier records a history of preference changes in the dashboard. Open the <AppLink href="https://app.courier.com/users">Users section</AppLink> and select a user. You see their subscription topics, current status, and a log of when each preference changed and who changed it.

The API returns **current state only**. <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences" /> gives you the live status and channel choices for every topic, but no change history. There is no "preference updated" webhook event, so poll the read endpoint to keep an external system in sync.

That history is per user. Your own team's changes to the configuration land in the workspace <Doc href="/docs/workspaces/audit-trail">audit trail</Doc> instead, including a topic created or deleted, its channels changed, and its default status changed.

## Limits & behavior

* **Bulk is per user, up to 50 topics.** Both bulk endpoints act on a single `user_id`. There is no cross-user bulk endpoint.
* **Digest schedules have no write endpoint.** Configure them in the editor. REST only triggers and lists digest instances.
* **No preference-change webhook.** History lives in the dashboard. The API exposes current state only.
* **`REQUIRED` is not a user choice, but it is a read value.** Courier rejects a write that opts a user out of a required topic. Reads return `"status": "REQUIRED"` for those topics because `status` falls back to the topic default.
* **Reads return every topic.** <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences" /> lists all topics in all sections, not just the ones the user has set.

## FAQ

<AccordionGroup>
  <Accordion title="Is there a bulk preferences endpoint?">
    Two bulk endpoints exist, both per user. <Endpoint method="PUT" path="/users/{user_id}/preferences" name="Replace user Preferences in bulk" href="/docs/api-reference/user-preferences/replace-user-preferences-in-bulk" /> replaces a user's full override set atomically. <Endpoint method="POST" path="/users/{user_id}/preferences" name="Update user Preferences in bulk" href="/docs/api-reference/user-preferences/update-user-preferences-in-bulk" /> updates topics additively. Each accepts up to 50 topics. No endpoint updates multiple users in one call, so a large import runs one bulk call per user.
  </Accordion>

  <Accordion title="Can I set a digest schedule through the API?">
    You configure digest schedules per topic in the <AppLink href="https://app.courier.com/~/test/platform/preferences">Preferences Editor</AppLink>. Over REST you can release a digest early and list its instances, but not create or change a schedule.
  </Accordion>

  <Accordion title="Is there a webhook for preference changes?">
    There is no preference-updated event. The dashboard shows change history, and <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences" /> returns current state, so poll it to sync an external system.
  </Accordion>

  <Accordion title="Where do topic IDs come from?">
    You create topics in the <AppLink href="https://app.courier.com/~/test/platform/preferences">Preferences Editor</AppLink> or the Workspace Preferences API, and each carries a TypeID prefixed `pt_`. Reference that `topic_id` when mapping templates and when reading or writing user preferences.
  </Accordion>
</AccordionGroup>
