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

# Notify everyone watching

> Model watchers of a document or ticket as a list per entity, then notify them in one send.

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

<Tags items="Lists, Preferences" />

Someone comments on a document, and everyone watching it should hear about it. This guide models watchers as a list per entity.

A `list_id` is a dotted namespace, so `document.doc_a1b2.watchers` names the watchers of one document. Subscribing a user creates that list, so there is nothing to set up first.

## Prerequisites

* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>
* A <Doc href="/docs/recipients/overview">profile</Doc> for each user you subscribe
* A published <Doc href="/docs/design/templates/overview">template</Doc> to send

## Name the list after the entity

Pick a naming convention before you write any code, because the name is the only thing tying a list to the thing it belongs to. A `list_id` holds up to six dot-separated segments:

```text theme={null}
<entity type>.<entity id>.<relationship>
```

| The thing               | The list                      |
| :---------------------- | :---------------------------- |
| Watchers of a document  | `document.doc_a1b2.watchers`  |
| Members of a project    | `project.proj_9f3e.members`   |
| Subscribers to a ticket | `ticket.tkt_77c1.subscribers` |

Keeping the entity type in the first segment is what makes <Guide href="/docs/guides/notify-everyone-watching#reach-every-entity-of-a-kind">wildcards</Guide> useful later. Entity ids with dots in them will not work, since a dot starts a new segment.

## Wire up watching

<Steps>
  <Step title="Subscribe a user when they start watching">
    One call, and it is idempotent. <Endpoint method="PUT" path="/lists/{list_id}/subscriptions/{user_id}" name="Subscribe a user Profile to a List" href="/docs/api-reference/lists/subscribe-a-user-profile-to-a-list" /> **creates the list if it does not exist**, so you never check first.

    <CodeGroup>
      ```javascript Node.js theme={null}
      await client.lists.subscriptions.subscribeUser("user_123", {
        list_id: "document.doc_a1b2.watchers",
      });
      ```

      ```python Python theme={null}
      client.lists.subscriptions.subscribe_user(
          "user_123",
          list_id="document.doc_a1b2.watchers",
      )
      ```

      ```bash cURL theme={null}
      curl -X PUT https://api.courier.com/lists/document.doc_a1b2.watchers/subscriptions/user_123 \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{}'
      ```

      ```ruby Ruby theme={null}
      courier.lists.subscriptions.subscribe_user(
        "user_123",
        list_id: "document.doc_a1b2.watchers"
      )
      ```

      ```go Go theme={null}
      err := client.Lists.Subscriptions.SubscribeUser(
      	context.TODO(),
      	"user_123",
      	courier.ListSubscriptionSubscribeUserParams{
      		ListID: "document.doc_a1b2.watchers",
      	},
      )
      ```

      ```java Java theme={null}
      SubscriptionSubscribeUserParams params = SubscriptionSubscribeUserParams.builder()
          .listId("document.doc_a1b2.watchers")
          .userId("user_123")
          .build();

      client.lists().subscriptions().subscribeUser(params);
      ```

      ```php PHP theme={null}
      $client->lists->subscriptions->subscribeUser(
        'user_123',
        listID: 'document.doc_a1b2.watchers',
      );
      ```

      ```csharp C# theme={null}
      await client.Lists.Subscriptions.SubscribeUser(
          "user_123",
          new() { ListID = "document.doc_a1b2.watchers" }
      );
      ```

      ```bash CLI theme={null}
      courier lists subscriptions subscribe-user \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123 \
        --list-id document.doc_a1b2.watchers
      ```

      ```text MCP theme={null}
      With Courier MCP, subscribe user_123 to the list document.doc_a1b2.watchers.
      ```
    </CodeGroup>

    Call this wherever your app already records a watch: an explicit **Watch** button, or implicitly when someone comments or is assigned.
  </Step>

  <Step title="Notify everyone watching">
    Address the list instead of a user. Courier fans out to every subscriber and applies each recipient's own <Doc href="/docs/recipients/preferences/overview">preferences</Doc>.

    <CodeGroup>
      ```javascript Node.js highlight={3} theme={null}
      await client.send.message({
        message: {
          to: { list_id: "document.doc_a1b2.watchers" },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: { document_title: "Q3 Roadmap", commenter: "Sarah Bennett" },
        },
      });
      ```

      ```python Python highlight={3} theme={null}
      client.send.message(
          message={
              "to": {"list_id": "document.doc_a1b2.watchers"},
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {"document_title": "Q3 Roadmap", "commenter": "Sarah Bennett"},
          },
      )
      ```

      ```bash cURL highlight={6} theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": { "list_id": "document.doc_a1b2.watchers" },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": { "document_title": "Q3 Roadmap", "commenter": "Sarah Bennett" }
          }
        }'
      ```

      ```ruby Ruby highlight={3} theme={null}
      courier.send_.message(
        message: {
          to: {list_id: "document.doc_a1b2.watchers"},
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {document_title: "Q3 Roadmap", commenter: "Sarah Bennett"}
        }
      )
      ```

      ```go Go highlight={5} theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfListRecipient: &shared.ListRecipientParam{
      				ListID: courier.String("document.doc_a1b2.watchers"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Data: map[string]any{
      			"document_title": "Q3 Roadmap",
      			"commenter":      "Sarah Bennett",
      		},
      	},
      })
      ```

      ```java Java highlight={3} theme={null}
      SendMessageParams.Message message = SendMessageParams.Message.builder()
          .to(SendMessageParams.Message.To.ofListRecipient(
              ListRecipient.builder().listId("document.doc_a1b2.watchers").build()))
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .data(SendMessageParams.Message.Data.builder()
              .putAdditionalProperty("document_title", JsonValue.from("Q3 Roadmap"))
              .putAdditionalProperty("commenter", JsonValue.from("Sarah Bennett"))
              .build())
          .build();

      client.send().message(SendMessageParams.builder().message(message).build());
      ```

      ```php PHP highlight={3} theme={null}
      $client->send->message(
        message: [
          'to' => ['list_id' => 'document.doc_a1b2.watchers'],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => ['document_title' => 'Q3 Roadmap', 'commenter' => 'Sarah Bennett'],
        ],
      );
      ```

      ```csharp C# highlight={5} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new ListRecipient { ListID = "document.doc_a1b2.watchers" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new()
              {
                  ["document_title"] = "Q3 Roadmap",
                  ["commenter"] = "Sarah Bennett",
              },
          },
      };

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

      ```bash CLI highlight={3} theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"list_id": "document.doc_a1b2.watchers"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
        --message.data '{"document_title": "Q3 Roadmap", "commenter": "Sarah Bennett"}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send the nt_01kx4h2jdafq8bk9aftxak4b40 template to the list document.doc_a1b2.watchers.
      ```
    </CodeGroup>

    One send reaches every watcher, however many there are. You do not fan out yourself.
  </Step>

  <Step title="Unsubscribe when they stop watching">
    <Endpoint method="DELETE" path="/lists/{list_id}/subscriptions/{user_id}" name="Unsubscribe a user Profile from a List" href="/docs/api-reference/lists/unsubscribe-a-user-profile-from-a-list" /> removes one watcher and leaves the rest alone.

    <CodeGroup>
      ```javascript Node.js theme={null}
      await client.lists.subscriptions.unsubscribeUser("user_123", {
        list_id: "document.doc_a1b2.watchers",
      });
      ```

      ```python Python theme={null}
      client.lists.subscriptions.unsubscribe_user(
          "user_123",
          list_id="document.doc_a1b2.watchers",
      )
      ```

      ```bash cURL theme={null}
      curl -X DELETE https://api.courier.com/lists/document.doc_a1b2.watchers/subscriptions/user_123 \
        -H "Authorization: Bearer $COURIER_API_KEY"
      ```

      ```ruby Ruby theme={null}
      courier.lists.subscriptions.unsubscribe_user(
        "user_123",
        list_id: "document.doc_a1b2.watchers"
      )
      ```

      ```go Go theme={null}
      err := client.Lists.Subscriptions.UnsubscribeUser(
      	context.TODO(),
      	"user_123",
      	courier.ListSubscriptionUnsubscribeUserParams{
      		ListID: "document.doc_a1b2.watchers",
      	},
      )
      ```

      ```java Java theme={null}
      SubscriptionUnsubscribeUserParams params = SubscriptionUnsubscribeUserParams.builder()
          .listId("document.doc_a1b2.watchers")
          .userId("user_123")
          .build();

      client.lists().subscriptions().unsubscribeUser(params);
      ```

      ```php PHP theme={null}
      $client->lists->subscriptions->unsubscribeUser(
        'user_123',
        listID: 'document.doc_a1b2.watchers',
      );
      ```

      ```csharp C# theme={null}
      await client.Lists.Subscriptions.UnsubscribeUser(
          "user_123",
          new() { ListID = "document.doc_a1b2.watchers" }
      );
      ```

      ```bash CLI theme={null}
      courier lists subscriptions unsubscribe-user \
        --api-key "$COURIER_API_KEY" \
        --user-id user_123 \
        --list-id document.doc_a1b2.watchers
      ```

      ```text MCP theme={null}
      With Courier MCP, unsubscribe user_123 from the list document.doc_a1b2.watchers.
      ```
    </CodeGroup>

    When the entity itself is deleted, delete the whole list with <Endpoint method="DELETE" path="/lists/{list_id}" name="Delete a List" href="/docs/api-reference/lists/delete-a-list" /> rather than removing watchers one at a time.
  </Step>
</Steps>

## Ask who is watching what

Both directions of the relationship are readable, which is what makes this usable as your app's own watch state rather than a write-only copy of it.

| Question                       | Endpoint                                                                                                                                                       |
| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Who is watching this document? | <Endpoint method="GET" path="/lists/{list_id}/subscriptions" name="List subscriptions for a List" href="/docs/api-reference/lists/list-subscriptions-for-a-list" /> |
| What is this user watching?    | <Endpoint method="GET" path="/profiles/{user_id}/lists" name="Get List subscriptions" href="/docs/api-reference/user-profiles/get-list-subscriptions" />            |

Both are <Doc href="/docs/reference/api-overview#pagination">cursor-paginated</Doc>.

## Reach every entity of a kind

Because the entity type leads the `list_id`, one pattern can address many lists at once. Swap `list_id` for `list_pattern`, where `*` matches a single segment:

```json theme={null}
{
  "to": { "list_pattern": "document.*.watchers" }
}
```

That reaches everyone watching any document, without you listing them. It is the reason the naming convention puts the type first and the relationship last.

<Note>
  A recipient watching several matched entities is deduplicated, so nobody receives the same message twice from one send.
</Note>

## Per-watcher preferences

A subscription can carry its own `preferences`, which apply to that subscription alone. Someone can follow a noisy project on digest while staying on instant notifications everywhere else. See <Doc href="/docs/recipients/preferences/overview">preferences</Doc> for the status model.

## Verify

<Steps>
  <Step title="Subscribe two users">
    Subscribe two test users to `document.doc_a1b2.watchers`.
  </Step>

  <Step title="Read the list back">
    Call <Endpoint method="GET" path="/lists/{list_id}/subscriptions" name="List subscriptions for a List" href="/docs/api-reference/lists/list-subscriptions-for-a-list" /> and confirm both appear, which also proves the list was created by the subscribe call.
  </Step>

  <Step title="Send and check the logs">
    Send to the list, then open <AppLink href="https://app.courier.com/logs">Logs</AppLink>. You should see one message per subscriber, each with its own status.
  </Step>

  <Step title="Unsubscribe one and send again">
    The second send reaches one recipient. The unsubscribed user gets nothing.
  </Step>
</Steps>

## FAQ

<AccordionGroup>
  <Accordion title="Do I have to create the list before subscribing anyone?">
    Subscribing creates the list when it does not exist, so a Watch button is one call with no setup and no existence check.
  </Accordion>

  <Accordion title="What happens when a watcher has opted out?">
    Courier applies each recipient's preferences during fan-out, so an opted-out watcher is filtered and the rest still receive the message. The send does not fail.
  </Accordion>

  <Accordion title="How many watchers can one list hold?">
    Lists are built for fan-out, so a busy document with thousands of watchers is one send. Read members back with cursor pagination rather than all at once.
  </Accordion>

  <Accordion title="Can I scope this to one tenant?">
    Add a `MEMBER_OF` filter on the recipient alongside `context.tenant_id`. Tenant context alone sets branding and preferences without restricting who receives the message. See <Doc href="/docs/recipients/lists-and-audiences/lists#scoping-a-list-send-to-a-tenant">scoping a list send to a tenant</Doc>.
  </Accordion>

  <Accordion title="Should I use an audience instead?">
    Use an audience when membership follows from profile data, such as every user on the Pro plan. Watching is an explicit act that no profile field implies, so a list is the right shape.
  </Accordion>
</AccordionGroup>
