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

# Build a preference center

> Let users choose their topics and channels with the hosted page or the embedded component.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

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 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>;
};

<Tags items="Inbox, Preferences" />

Give users one place to choose which messages they receive, and on which channels.

Configure topics once in the editor. Then show them as a Courier-hosted page or an embedded component. Both read and write the same preference data.

## Prerequisites

* A Courier workspace
* <AppLink href="https://app.courier.com/content/templates">The templates you want users to control</AppLink>
* <Doc href="/docs/recipients/preferences/model">The preferences model</Doc>
* <AppLink href="https://app.courier.com/content/brands">An optional brand</AppLink>

## Configure the topics

Use the <AppLink href="https://app.courier.com/~/test/platform/preferences">preferences editor</AppLink> (**Platform → Preferences**). Topics, sections, and branding are all defined here. Every setting it exposes is in <Doc href="/docs/recipients/preferences/preferences-editor">the Preferences Editor reference</Doc>.

<Frame caption="The preferences editor: page settings, sections, and topics, with a live preview of the hosted page.">
  <img src="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/preferences-editor.webp?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=24a120335981c1da4bb1671cb8eba5c2" alt="The preferences editor: brand and heading settings and section tabs on the left, with a live preview of the hosted page on the right" width="2406" height="1616" data-path="assets/preferences-editor.webp" />
</Frame>

<Steps>
  <Step title="Set page settings">
    Page settings apply to the whole page:

    * An optional <Doc href="/docs/design/brands">brand</Doc>, for logo and colors.
    * The heading and description.
    * The channel names users see. Rename them to match your product, for example "In-App" to "Inbox".
  </Step>

  <Step title="Add sections">
    Sections group related topics under a heading you name, such as "Account" or "Marketing". Turn on channel selection for a section to let users pick which channels deliver its topics.
  </Step>

  <Step title="Add topics">
    Topics are the categories users opt in or out of. For each one, set a name, its section, and a default state:

    * **On (`OPTED_IN`):** users receive it unless they opt out.
    * **Off (`OPTED_OUT`):** users receive nothing until they opt in.
    * **Required (`REQUIRED`):** users cannot opt out. Use it for critical messages, and leave unsubscribe links off templates mapped to required topics.
  </Step>

  <Step title="Map templates to topics">
    Mapping a template to a topic makes Courier enforce the preference on every send. Set it in a template's <Doc href="/docs/design/templates/design-studio">settings</Doc> in Design Studio, or set the template's `subscription` field to `{ "topic_id": "<TOPIC_ID>" }` through the API.
  </Step>

  <Step title="Publish">
    Every change saves as a draft. Use **Preview Page** to open the page in draft mode, then **Publish** to make it live. Until you publish, users see the last published version.
  </Step>
</Steps>

To batch a topic's messages on a schedule, open the topic's digest settings and link a digest template. See the <Doc href="/docs/recipients/preferences/model#digest-schedules">digest schedules model</Doc> for how batching, categories, and Trigger empty behave.

## Hosted page

The hosted preference center needs no frontend. It lists the user's subscription topics with the heading, description, and optional brand you set in the editor. It works on desktop and mobile. Courier serves it at `https://view.notificationcenter.app/p/<id>`. The id encodes the workspace, brand, and user, so each recipient sees their own preferences.

<Frame caption="The Courier-hosted preference page a user opens from the preferences link: their subscription topics, grouped into sections and branded.">
  <img src="https://mintcdn.com/courier-4f1f25dc/9rcgucLA9fBnJt_U/assets/hosted-preference-center-superset.webp?fit=max&auto=format&n=9rcgucLA9fBnJt_U&q=85&s=03ee1792c8d4582d4b1f77b332770590" alt="A branded Courier-hosted preference page with Account Notifications and Marketing sections, per-topic toggles, a Required topic, and a digest delivery schedule" className="mx-auto" style={{ width:"62%" }} width="1222" height="1570" data-path="assets/hosted-preference-center-superset.webp" />
</Frame>

## Link users to the preference center

Link users to their hosted page with the `preferences` variable. Courier renders it as a secure, per-user URL. Include `to.user_id` in your send so each recipient gets their own link. Write the variable in the format for your editing context:

* Content blocks (Text, Markdown, Quote, List): `{{$.urls.preferences}}`
* Handlebars (Template blocks, email and brand templates): `{{var "urls.preferences"}}`
* Elemental JSON (action buttons and links): `{$.urls.preferences}`

Put the variable in a saved template, then send that template with the <Endpoint method="POST" path="/send" name="Send a message" href="/docs/api-reference/send/send-a-message">Send API</Endpoint>. Below, `nt_01kx4h2jdafq8bk9aftxak4b40` is a template whose body includes `{{$.urls.preferences}}`:

<CodeGroup>
  ```javascript Node.js theme={null}
  import Courier from "@trycourier/courier";

  const client = new Courier({
    apiKey: process.env["COURIER_API_KEY"],
  });

  const { requestId } = await client.send.message({
    message: {
      to: { user_id: "user_123" },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
    },
  });

  console.log(requestId);
  ```

  ```python Python theme={null}
  import os

  from courier import Courier

  client = Courier(
      api_key=os.environ.get("COURIER_API_KEY"),
  )
  response = client.send.message(
      message={
          "to": {"user_id": "user_123"},
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
      },
  )
  print(response.request_id)
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "to": { "user_id": "user_123" },
        "template": "nt_01kx4h2jdafq8bk9aftxak4b40"
      }
    }'
  ```

  ```ruby Ruby theme={null}
  require "courier"

  courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

  response = courier.send_.message(
    message: {
      to: { user_id: "user_123" },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40"
    }
  )

  puts(response)
  ```

  ```go Go theme={null}
  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				UserID: courier.String("user_123"),
  			},
  		},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  	},
  })
  ```

  ```java Java theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("user_id", "user_123")))
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .build())
      .build();

  var response = client.send().message(params);
  ```

  ```php PHP theme={null}
  $response = $client->send->message(
    message: [
      'to' => ['userID' => 'user_123'],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
    ],
  );
  ```

  ```csharp C# theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message '{"to":{"user_id":"user_123"},"template":"nt_01kx4h2jdafq8bk9aftxak4b40"}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123.
  ```
</CodeGroup>

For a one-click opt-out from the topic tied to a message, use the `unsubscribe` variable the same way (`{{$.urls.unsubscribe}}`, `{{var "urls.unsubscribe"}}`, or `{$.urls.unsubscribe}`).

<Warning>
  The URL is empty when the template has no subscription topic, so <Doc href="/docs/design/templates/design-studio">assign a topic</Doc> first. On a required topic the opt-out does nothing, so leave unsubscribe links off those templates.
</Warning>

Place either link inside a message, or once in your <Doc href="/docs/design/brands">brand</Doc> footer so it appears on every message.

### One-click unsubscribe headers

Gmail and Yahoo require bulk senders to support one-click unsubscribe. Turn on **Unsubscribe Headers** for a topic in the preferences editor, or set `include_unsubscribe_header` on the topic through the API. Courier then adds two headers to that topic's email:

```text theme={null}
List-Unsubscribe: <the recipient's unsubscribe URL>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
```

Both conditions have to hold for the headers to appear. The topic has the setting on, and the message resolves an unsubscribe URL, which means the template is mapped to that topic.

<Note>
  SendGrid is the only provider Courier emits these headers on today. On another email provider, set them yourself with a <Doc href="/docs/send/overrides">provider override</Doc>.
</Note>

## Embedded component

To keep users inside your product, embed the preference center instead of linking to the hosted page. It reads the same published topics and writes the same preference data, so a change in one place shows up in the other.

<Frame caption="The embedded preference center component: subscription topics with per-topic channel choices.">
  <img src="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/preferences-default.webp?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=d31b2fb8c91c01bd97f26b09d0ecb4bc" alt="The embeddable Courier preferences component, showing a topic section with per-topic toggles and an expanded channel picker for email, push, and SMS" className="mx-auto" width="3152" height="1776" data-path="assets/preferences-default.webp" />
</Frame>

Setup, theming, and building your own UI live in the In-App Notifications tab:

<CardGroup cols={2}>
  <Card title="Add a preference center" icon="react" href="/docs/in-app/add-a-preference-center">
    Install, authenticate, and render the component on every platform.
  </Card>

  <Card title="Customize Preferences" icon="paintbrush" href="/docs/in-app/customize-preferences">
    Theme the component or build your own preferences UI.
  </Card>
</CardGroup>

## Verify

<Steps>
  <Step title="Open the preference center">
    Open the hosted page from a real send (a message containing `{{$.urls.preferences}}` with `to.user_id` set), or load your embedded component as a test user.
  </Step>

  <Step title="Toggle a topic off and send">
    Toggle a topic off. Send a message mapped to that topic and confirm the log shows the send blocked as `UNSUBSCRIBED`.
  </Step>

  <Step title="Read the change back">
    <Endpoint method="GET" path="/users/{user_id}/preferences" name="Get user's Preferences" href="/docs/api-reference/user-preferences/get-users-preferences">Read the change back</Endpoint> and confirm the `status` matches what you toggled.
  </Step>
</Steps>
