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

# Tenant default preferences

> Set default topic statuses per tenant that apply before a user makes their own choice.

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

<Info>
  <Doc href="/docs/recipients/preferences/overview">How preferences resolve</Doc> and <Doc href="/docs/tenants/context">how tenant context works</Doc> cover the concepts behind this page.
</Info>

Preferences are usually the first reason people reach for tenants. A customer's admin wants a category of notification switched off across their whole company, and that has to hold for users who never open a preference center.

A tenant default sets a topic's starting state for everyone in that organization. It overrides the workspace default for any send that names the tenant, and each user can still change their own from there.

**A tenant default is not a way to force a setting.** A user who opts out stays opted out, because their own preference sits below the tenant's in the <Doc href="/docs/recipients/preferences/overview#status-precedence">merge order</Doc>. Mark a topic `REQUIRED` when it is transactional and nobody should be able to switch it off, which is the only status that ignores what the user chose.

<Warning>
  A tenant default sets a topic's **status** and nothing else. Channel routing is read from each user's own preference record. A tenant's `has_custom_routing` and `custom_routing` have no effect at send time, even though the API accepts and returns them, so a tenant that looks correctly configured can route nothing. To route a topic to specific channels, set it on the <Doc href="/docs/recipients/preferences/api">user's preferences</Doc>.
</Warning>

## Prerequisites

* <AppLink href="https://app.courier.com/directory/tenants">A tenant under Directory → Tenants</AppLink>
* <Doc href="/docs/tenants/overview">The tenant model</Doc>
* <AppLink href="https://app.courier.com/~/test/platform/preferences">Your subscription topics under Platform → Preferences</AppLink>
* <Doc href="/docs/recipients/preferences/overview#status-precedence">The status precedence rules</Doc>

## Set tenant defaults

<Steps>
  <Step title="Set defaults in the console">
    In the console, a tenant stores its defaults as a single `default_preferences` JSON object that you edit directly. Go to the tenant in the <AppLink href="https://app.courier.com/directory/tenants">console</AppLink> (**Directory → Tenants**) and open its **Details** tab. In the **Default Preferences** section, paste a JSON object whose `items` array holds one entry per topic. Each entry takes the `topic_id` as `id` and a `status` of `OPTED_IN`, `OPTED_OUT`, or `REQUIRED`. Save.

    <Frame caption="The Default Preferences JSON editor on a tenant's Details tab in the console.">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/tenant-default-preferences.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=275ba127bd82e12171112f9077d377cc" alt="A tenant's Details tab with the Default Preferences JSON editor holding three topic entries and reporting Valid JSON" className="mx-auto" style={{ width:"70%" }} width="982" height="1192" data-path="assets/tenant-default-preferences.webp" />
    </Frame>

    This example opts one topic in, makes a second impossible to turn off, and opts out of a third. Topics you leave out keep their workspace default:

    ```json theme={null}
    {
      "items": [
        {
          "id": "pt_01kx4h2jdafq8bk996nn92357r",
          "status": "OPTED_IN"
        },
        {
          "id": "pt_01kx4h9m2rdvc7yq5tb8n30k4e",
          "status": "REQUIRED"
        },
        {
          "id": "pt_01kx4hb7t8fzs2mp6wc4j91xgd",
          "status": "OPTED_OUT"
        }
      ]
    }
    ```
  </Step>

  <Step title="Set a default with the API">
    <Endpoint method="PUT" path="/tenants/{tenant_id}/default_preferences/items/{topic_id}" name="Create or replace default Preferences for Topic" href="/docs/api-reference/tenants/create-or-replace-default-preferences-for-topic" /> sets tenant defaults one topic at a time from your backend. Only the topics you include override the workspace default. Topics you leave out fall back to their workspace default state. The same `default_preferences` object shown above is what you send when you create or replace the tenant through the <Endpoint method="PUT" path="/tenants/{tenant_id}" name="Create or replace a Tenant" href="/docs/api-reference/tenants/create-or-replace-a-tenant">Tenants API</Endpoint>. Set one topic's default with a body shaped like a topic, carrying its `status`.

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

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

      await client.tenants.preferences.items.update('pt_01kx4h2jdafq8bk996nn92357r', {
        tenant_id: 'acme-corp',
        status: 'OPTED_IN',
      });
      ```

      ```python Python highlight={10} theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      client.tenants.preferences.items.update(
          topic_id="pt_01kx4h2jdafq8bk996nn92357r",
          tenant_id="acme-corp",
          status="OPTED_IN",
      )
      ```

      ```bash cURL highlight={4} theme={null}
      curl -X PUT https://api.courier.com/tenants/acme-corp/default_preferences/items/pt_01kx4h2jdafq8bk996nn92357r \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{ "status": "OPTED_IN" }'
      ```

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

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

      result = courier.tenants.preferences.items.update("pt_01kx4h2jdafq8bk996nn92357r", tenant_id: "acme-corp", status: :OPTED_IN)

      puts(result)
      ```

      ```go Go highlight={7} theme={null}
      err := client.Tenants.Preferences.Items.Update(
      	context.TODO(),
      	"pt_01kx4h2jdafq8bk996nn92357r",
      	courier.TenantPreferenceItemUpdateParams{
      		TenantID: "acme-corp",
      		SubscriptionTopicNew: courier.SubscriptionTopicNewParam{
      			Status: courier.SubscriptionTopicNewStatusOptedIn,
      		},
      	},
      )
      ```

      ```java Java highlight={5} theme={null}
      ItemUpdateParams params = ItemUpdateParams.builder()
          .tenantId("acme-corp")
          .topicId("pt_01kx4h2jdafq8bk996nn92357r")
          .subscriptionTopicNew(SubscriptionTopicNew.builder()
              .status(SubscriptionTopicNew.Status.OPTED_IN)
              .build())
          .build();
      client.tenants().preferences().items().update(params);
      ```

      ```php PHP highlight={4} theme={null}
      $result = $client->tenants->preferences->items->update(
        'pt_01kx4h2jdafq8bk996nn92357r',
        tenantID: 'acme-corp',
        status: 'OPTED_IN',
      );
      ```

      ```csharp C# highlight={5} theme={null}
      ItemUpdateParams parameters = new()
      {
          TenantID = "acme-corp",
          TopicID = "pt_01kx4h2jdafq8bk996nn92357r",
          Status = Status.OptedIn,
      };

      await client.Tenants.Preferences.Items.Update(parameters);
      ```

      ```bash CLI highlight={5} theme={null}
      courier tenants:preferences:items update \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp \
        --topic-id pt_01kx4h2jdafq8bk996nn92357r \
        --status OPTED_IN
      ```

      ```text MCP theme={null}
      With Courier MCP, opt acme-corp into that topic by default.
      ```
    </CodeGroup>
  </Step>

  <Step title="Remove a tenant default">
    <Endpoint method="DELETE" path="/tenants/{tenant_id}/default_preferences/items/{topic_id}" name="Remove default Preferences for Topic" href="/docs/api-reference/tenants/remove-default-preferences-for-topic" /> removes a tenant default for a topic, reverting it to the workspace default:

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

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

      await client.tenants.preferences.items.delete('pt_01kx4h2jdafq8bk996nn92357r', { tenant_id: 'acme-corp' });
      ```

      ```python Python theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      client.tenants.preferences.items.delete(
          topic_id="pt_01kx4h2jdafq8bk996nn92357r",
          tenant_id="acme-corp",
      )
      ```

      ```bash cURL theme={null}
      curl -X DELETE https://api.courier.com/tenants/acme-corp/default_preferences/items/pt_01kx4h2jdafq8bk996nn92357r \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

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

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

      result = courier.tenants.preferences.items.delete("pt_01kx4h2jdafq8bk996nn92357r", tenant_id: "acme-corp")

      puts(result)
      ```

      ```go Go theme={null}
      err := client.Tenants.Preferences.Items.Delete(
      	context.TODO(),
      	"pt_01kx4h2jdafq8bk996nn92357r",
      	courier.TenantPreferenceItemDeleteParams{
      		TenantID: "acme-corp",
      	},
      )
      ```

      ```java Java theme={null}
      ItemDeleteParams params = ItemDeleteParams.builder()
          .tenantId("acme-corp")
          .topicId("pt_01kx4h2jdafq8bk996nn92357r")
          .build();
      client.tenants().preferences().items().delete(params);
      ```

      ```php PHP theme={null}
      $result = $client->tenants->preferences->items->delete(
        'pt_01kx4h2jdafq8bk996nn92357r', tenantID: 'acme-corp'
      );
      ```

      ```csharp C# theme={null}
      ItemDeleteParams parameters = new()
      {
          TenantID = "acme-corp",
          TopicID = "pt_01kx4h2jdafq8bk996nn92357r",
      };

      await client.Tenants.Preferences.Items.Delete(parameters);
      ```

      ```bash CLI theme={null}
      courier tenants:preferences:items delete \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp \
        --topic-id pt_01kx4h2jdafq8bk996nn92357r
      ```

      ```text MCP theme={null}
      With Courier MCP, remove acme-corp's default for that topic.
      ```
    </CodeGroup>
  </Step>

  <Step title="Read the defaults back">
    Read the tenant back to confirm its defaults. The `default_preferences` field on the returned tenant holds the topics you set. Call the <Endpoint method="GET" path="/tenants/{tenant_id}" name="Get a Tenant" href="/docs/api-reference/tenants/get-a-tenant">get tenant endpoint</Endpoint>:

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

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

      const tenant = await client.tenants.retrieve('acme-corp');

      console.log(tenant.default_preferences);
      ```

      ```python Python theme={null}
      import os
      from courier import Courier

      client = Courier(
          api_key=os.environ.get("COURIER_API_KEY"),
      )
      tenant = client.tenants.retrieve("acme-corp")
      print(tenant.default_preferences)
      ```

      ```bash cURL theme={null}
      curl -X GET https://api.courier.com/tenants/acme-corp \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

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

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

      tenant = courier.tenants.retrieve("acme-corp")

      puts(tenant.default_preferences)
      ```

      ```go Go theme={null}
      tenant, err := client.Tenants.Get(context.TODO(), "acme-corp")
      ```

      ```java Java theme={null}
      Tenant tenant = client.tenants().retrieve("acme-corp");
      ```

      ```php PHP theme={null}
      $tenant = $client->tenants->retrieve('acme-corp');
      ```

      ```csharp C# theme={null}
      TenantRetrieveParams parameters = new() { TenantID = "acme-corp" };

      var tenant = await client.Tenants.Retrieve(parameters);
      ```

      ```bash CLI theme={null}
      courier tenants retrieve \
        --api-key "$COURIER_API_KEY" \
        --tenant-id acme-corp
      ```

      ```text MCP theme={null}
      With Courier MCP, show me acme-corp's tenant settings and default preferences.
      ```
    </CodeGroup>
  </Step>

  <Step title="Verify the tenant default">
    Pick a user with no preference set for the Topic. Send them a message mapped to it in the Tenant's context, including `tenant_id`. Confirm delivery follows the Tenant default, not the workspace default.
  </Step>
</Steps>

## Limits & behavior

* **Reading a user's preferences does not show you the tenant default.** The call returns the topic-level `default_status` even when <Doc href="/docs/recipients/preferences/api#scope-to-a-tenant">scoped to the tenant</Doc>, so a tenant default you set correctly looks absent from the user's side. Read the tenant itself for its defaults, and use a test send to confirm what a user receives.
* **Topics you leave out keep the workspace default.** `default_preferences` is a partial override rather than a full replacement, so removing a topic from it restores the workspace state rather than clearing the topic.
* **A tenant default only applies to sends that name the tenant.** The same user, sent to without a `tenant_id`, falls back to the workspace default unless auto-infer resolves the tenant for you.
