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

# Update or create user Preferences for a Subscription Topic

> Sets a user's opt-in status and channel choices for one subscription topic, overriding the tenant default for that topic only.



## OpenAPI

````yaml /openapi-specs/openapi.documented.yml put /users/{user_id}/preferences/{topic_id}
openapi: 3.0.1
info:
  title: Courier
  description: The Courier REST API for sending and managing notifications across channels.
  version: 0.0.1
servers:
  - url: https://api.courier.com
    description: Production
security: []
tags:
  - name: Send
    description: >-
      Send a message to one or more recipients — users, lists, audiences, or
      tenants — across every channel you have configured.
  - name: Templates
    description: >-
      Create, update, version, publish, and localize notification templates and
      their content.
  - name: Brands
    description: >-
      Manage the logos, colors, and layout that give the templates you send a
      consistent look.
  - name: Routing Strategies
    description: >-
      Define reusable channel routing and failover strategies, and see which
      templates use them.
  - name: Journeys
    description: >-
      Build, version, publish, invoke, and cancel multi-step notification
      workflows, along with the templates scoped to them.
  - name: Broadcasts
    description: >-
      Create a one-off send to a list or audience, author its content, then send
      it immediately or schedule it for later.
  - name: Previews
    description: >-
      Render a template's email content on real email clients and read back the
      screenshots, so you can check how it looks before you send it.
  - name: User Profiles
    description: >-
      Store the contact information Courier delivers to for each user — email,
      phone number, push tokens, and any custom data you send to.
  - name: Tenants
    description: >-
      Manage tenants — the organizations, teams, or accounts your users belong
      to — along with their users and default preferences.
  - name: Audiences
    description: >-
      Define filter-based groups whose membership Courier recalculates as user
      profiles change.
  - name: Lists
    description: >-
      Manage static groups of users that you subscribe explicitly, and send to
      them by list id or list pattern.
  - name: Providers
    description: >-
      Configure the channel providers Courier delivers through, and browse the
      provider types it supports.
  - name: Preference Topics
    description: >-
      Manage the workspace catalog of subscription topics, the sections that
      group them, and publishing the preference page.
  - name: User Preferences
    description: >-
      Read and write a single user's notification preferences, per topic and per
      channel.
  - name: Messages
    description: >-
      Look up the messages Courier has accepted, inspect their delivery history
      and rendered output, and cancel, resend, or archive them.
  - name: Device Tokens
    description: >-
      Register and manage the APNS and FCM device tokens Courier delivers push
      notifications to.
  - name: Tenant Memberships
    description: >-
      Associate a user with one or more tenants, and read or remove those
      associations.
  - name: Tenant Templates
    description: >-
      Manage the templates and template versions scoped to a single tenant,
      including the ones authored in the embedded designer.
  - name: Automations
    description: >-
      Invoke a stored automation template or an ad hoc automation defined in the
      request.
  - name: Digests
    description: >-
      Inspect what has accumulated in a digest schedule and release a digest
      ahead of its next scheduled delivery.
  - name: Translations
    description: >-
      Store and retrieve the translation strings Courier uses to render
      localized template content.
  - name: Track Events
    description: >-
      Record an inbound event that triggers the journeys and automations mapped
      to it.
  - name: Audit Events
    description: >-
      Read the audit trail of configuration and access changes in your
      workspace.
  - name: Authentication
    description: >-
      Issue scoped, short-lived JWTs so client-side SDKs — Inbox, Preferences,
      and the embedded designer — can call Courier as a single user. Server-side
      requests authenticate with your workspace API key instead.
paths:
  /users/{user_id}/preferences/{topic_id}:
    put:
      tags:
        - User Preferences
      summary: Update or create user Preferences for a Subscription Topic
      description: >-
        Sets a user's opt-in status and channel choices for one subscription
        topic, overriding the tenant default for that topic only.
      operationId: users_preferences_update
      parameters:
        - name: user_id
          in: path
          description: >-
            A unique identifier associated with the user whose preferences you
            wish to retrieve.
          required: true
          schema:
            type: string
          examples:
            Example1:
              value: abc-123
        - name: topic_id
          in: path
          description: A unique identifier associated with a subscription topic.
          required: true
          schema:
            type: string
          examples:
            Example1:
              value: pt_01kx4h2jdafq8bk996nn92357r
        - name: tenant_id
          in: query
          description: Update the preferences of a user for this specific tenant context.
          required: false
          schema:
            type: string
            nullable: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                topic:
                  $ref: '#/components/schemas/UsersTopicPreferenceUpdate'
              required:
                - topic
            examples:
              Example1:
                value:
                  topic:
                    status: OPTED_IN
                    has_custom_routing: true
                    custom_routing:
                      - inbox
                      - email
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsersUserPreferencesUpdateResponse'
              examples:
                Example1:
                  value:
                    message: success
        '400':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BadRequest'
              examples:
                Example:
                  value:
                    message: Example message text
                    type: invalid_request_error
      security:
        - BearerAuth: []
      x-codeSamples:
        - lang: JavaScript
          source: >-
            import Courier from '@trycourier/courier';


            const client = new Courier({
              apiKey: process.env['COURIER_API_KEY'], // This is the default and can be omitted
            });


            const response = await
            client.users.preferences.updateOrCreateTopic('topic_id', {
              user_id: 'user_id',
              topic: {
                status: 'OPTED_IN',
                has_custom_routing: true,
                custom_routing: ['inbox', 'email'],
              },
            });


            console.log(response.message);
        - lang: Python
          source: |-
            import os
            from courier import Courier

            client = Courier(
                api_key=os.environ.get("COURIER_API_KEY"),  # This is the default and can be omitted
            )
            response = client.users.preferences.update_or_create_topic(
                topic_id="topic_id",
                user_id="user_id",
                topic={
                    "status": "OPTED_IN",
                    "has_custom_routing": True,
                    "custom_routing": ["inbox", "email"],
                },
            )
            print(response.message)
        - lang: Go
          source: "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/trycourier/courier-go/v4\"\n\t\"github.com/trycourier/courier-go/v4/option\"\n\t\"github.com/trycourier/courier-go/v4/shared\"\n)\n\nfunc main() {\n\tclient := courier.NewClient(\n\t\toption.WithAPIKey(\"My API Key\"),\n\t)\n\tresponse, err := client.Users.Preferences.UpdateOrNewTopic(\n\t\tcontext.TODO(),\n\t\t\"topic_id\",\n\t\tcourier.UserPreferenceUpdateOrNewTopicParams{\n\t\t\tUserID: \"user_id\",\n\t\t\tTopic: courier.UserPreferenceUpdateOrNewTopicParamsTopic{\n\t\t\t\tStatus:           shared.PreferenceStatusOptedIn,\n\t\t\t\tHasCustomRouting: courier.Bool(true),\n\t\t\t\tCustomRouting:    []shared.ChannelClassification{shared.ChannelClassificationInbox, shared.ChannelClassificationEmail},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf(\"%+v\\n\", response.Message)\n}\n"
        - lang: Java
          source: >-
            package com.courier.example;


            import com.courier.client.CourierClient;

            import com.courier.client.okhttp.CourierOkHttpClient;

            import com.courier.models.PreferenceStatus;

            import
            com.courier.models.users.preferences.PreferenceUpdateOrCreateTopicParams;

            import
            com.courier.models.users.preferences.PreferenceUpdateOrCreateTopicResponse;


            public final class Main {
                private Main() {}

                public static void main(String[] args) {
                    CourierClient client = CourierOkHttpClient.fromEnv();

                    PreferenceUpdateOrCreateTopicParams params = PreferenceUpdateOrCreateTopicParams.builder()
                        .userId("user_id")
                        .topicId("topic_id")
                        .topic(PreferenceUpdateOrCreateTopicParams.Topic.builder()
                            .status(PreferenceStatus.OPTED_IN)
                            .build())
                        .build();
                    PreferenceUpdateOrCreateTopicResponse response = client.users().preferences().updateOrCreateTopic(params);
                }
            }
        - lang: Ruby
          source: |-
            require "courier"

            courier = Courier::Client.new(api_key: "My API Key")

            response = courier.users.preferences.update_or_create_topic(
              "topic_id",
              user_id: "user_id",
              topic: {status: :OPTED_IN}
            )

            puts(response)
        - lang: PHP
          source: >-
            <?php


            require_once dirname(__DIR__) . '/vendor/autoload.php';


            use Courier\Client;

            use Courier\PreferenceStatus;

            use Courier\ChannelClassification;

            use Courier\Core\Exceptions\APIException;


            $client = new Client(apiKey: getenv('COURIER_API_KEY') ?: 'My API
            Key');


            try {
              $response = $client->users->preferences->updateOrCreateTopic(
                'topic_id',
                userID: 'user_id',
                topic: [
                  'status' => PreferenceStatus::OPTED_IN,
                  'customRouting' => [
                    ChannelClassification::INBOX, ChannelClassification::EMAIL
                  ],
                  'digestScheduleID' => 'x',
                  'hasCustomRouting' => true,
                ],
                tenantID: 'tenant_id',
              );

              var_dump($response);
            } catch (APIException $e) {
              echo $e->getMessage();
            }
        - lang: C#
          source: >-
            using System;

            using TryCourier;

            using TryCourier.Models;

            using TryCourier.Models.Users.Preferences;


            CourierClient client = new();


            PreferenceUpdateOrCreateTopicParams parameters = new()

            {
                UserID = "user_id",
                TopicID = "topic_id",
                Topic = new()
                {
                    Status = PreferenceStatus.OptedIn,
                    CustomRouting =
                    [
                        ChannelClassification.Inbox, ChannelClassification.Email
                    ],
                    DigestScheduleID = "x",
                    HasCustomRouting = true,
                },
            };


            var response = await
            client.Users.Preferences.UpdateOrCreateTopic(parameters);


            Console.WriteLine(response);
        - lang: CLI
          source: |-
            courier users:preferences update-or-create-topic \
              --api-key 'My API Key' \
              --user-id user_id \
              --topic-id topic_id \
              --topic '{status: OPTED_IN}'
components:
  schemas:
    UsersTopicPreferenceUpdate:
      title: UsersTopicPreferenceUpdate
      type: object
      properties:
        digest_schedule_id:
          type: string
          minLength: 1
          nullable: true
          description: >-
            Put this recipient on one of the topic's digest schedules. Send
            `null` to clear the choice and return them to the topic's default.
            Omit to leave an existing choice alone, unlike the routing fields,
            which this endpoint replaces. An id that is not an active schedule
            on the topic is rejected with a `400` before anything is written.
        status:
          allOf:
            - $ref: '#/components/schemas/PreferenceStatus'
          description: >-
            The subscription status to set: OPTED_IN or OPTED_OUT. REQUIRED is a
            topic-level default, not a user choice; the API rejects opting a
            user out of a REQUIRED topic.
        custom_routing:
          type: array
          items:
            $ref: '#/components/schemas/ChannelClassification'
          nullable: true
          description: >-
            The channels to deliver this topic on when has_custom_routing is
            true. One or more of: direct_message, email, push, sms, webhook,
            inbox.
        has_custom_routing:
          type: boolean
          nullable: true
          description: >-
            Set to true to route this topic to the channels in custom_routing
            instead of the topic's default routing.
      required:
        - status
    UsersUserPreferencesUpdateResponse:
      title: UsersUserPreferencesUpdateResponse
      type: object
      properties:
        message:
          type: string
          example: success
      required:
        - message
    BadRequest:
      title: BadRequest
      type: object
      properties:
        type:
          type: string
          enum:
            - invalid_request_error
      required:
        - type
      allOf:
        - $ref: '#/components/schemas/BaseError'
    PreferenceStatus:
      title: PreferenceStatus
      type: string
      enum:
        - OPTED_IN
        - OPTED_OUT
        - REQUIRED
    ChannelClassification:
      title: ChannelClassification
      type: string
      enum:
        - direct_message
        - email
        - push
        - sms
        - webhook
        - inbox
    BaseError:
      title: BaseError
      type: object
      properties:
        message:
          type: string
          description: A message describing the error that occurred.
      required:
        - message
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer

````