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

# Send your first Slack message

> Create a Slack app, store its token on the recipient's profile, and send a direct message.

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 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="Chat, Users" />

Slack needs one thing the other channels do not: an app you create, installed into the workspace you want to post in.

Once its bot token is on a profile, a Slack message is the same send as an email.

## Prerequisites

* <AppLink href="https://app.courier.com/~/test/platform/api-keys">A Courier Test API key</AppLink>
* A Slack workspace you can install an app into
* A published <Doc href="/docs/design/templates/overview">template</Doc> with a Slack channel

## The token belongs to the recipient

This is the part that surprises people, and it is worth understanding before you start.

Most providers hold one credential on the integration. Slack passes its bot token **with each recipient**, on `to.slack.access_token`. Install the Slack integration once, then store a token per user or per customer.

That is what makes Slack workable for B2B. One Courier workspace posts into many customers' Slack workspaces, because each recipient carries the token for the workspace they belong to. A single shared credential could not do that.

## Send it

<Steps>
  <Step title="Install the integration and create a Slack app">
    Add **Slack** from <AppLink href="https://app.courier.com/integrations/catalog">Integrations</AppLink> in Courier.

    Then create the app on Slack's side. At [api.slack.com/apps](https://api.slack.com/apps), choose **Create an App**, then **From scratch**, and pick your workspace.

    Under **OAuth & Permissions**, add these Bot Token Scopes:

    | Scope              | Why                                    |
    | :----------------- | :------------------------------------- |
    | `chat:write`       | Post messages                          |
    | `im:write`         | Open a direct message channel          |
    | `users:read`       | Look up workspace members              |
    | `users:read.email` | Target a person by their email address |

    Install the app to the workspace, then copy the **Bot User OAuth Token**. It starts with `xoxb-`.

    <Doc href="/docs/integrations/direct-message/slack">The Slack integration page</Doc> has the screenshots for each step.
  </Step>

  <Step title="Put the token and a target on the profile">
    Store the token alongside how you want to reach the person. Courier accepts three targets, and uses the first one it finds in this order: `channel`, then `user_id`, then `email`. <Doc href="/docs/recipients/overview#what-each-channel-needs">Users</Doc> covers profile storage for every channel.

    ```json Profile theme={null}
    {
      "profile": {
        "slack": {
          "access_token": "xoxb-your-bot-token",
          "email": "sarah@acme-corp.com"
        }
      }
    }
    ```

    Which target to pick is a real decision rather than a preference:

    | Target    | Reach it with                 | Worth knowing                                                |
    | :-------- | :---------------------------- | :----------------------------------------------------------- |
    | `email`   | The address they use in Slack | Simplest, and breaks if their Slack email differs from yours |
    | `user_id` | Their Slack member id, `U…`   | Stable across email changes, and needs a lookup once         |
    | `channel` | A channel id, `C…`            | Posts to a room rather than a person                         |

    Start with `email` and move to `user_id` when a mismatch bites. The `users:read.email` scope is what makes email targeting work at all.
  </Step>

  <Step title="Send the message">
    Address the user and route to the Slack provider. Courier reads `slack` off the profile.

    <CodeGroup>
      ```javascript Node.js highlight={4} theme={null}
      const { requestId } = await client.send.message({
        message: {
          to: { user_id: "user_123" },
          routing: { method: "single", channels: ["slack"] },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        },
      });
      ```

      ```python Python highlight={4} theme={null}
      response = client.send.message(
          message={
              "to": {"user_id": "user_123"},
              "routing": {"method": "single", "channels": ["slack"]},
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      )
      ```

      ```bash cURL highlight={7} 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" },
            "routing": { "method": "single", "channels": ["slack"] },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40"
          }
        }'
      ```

      ```ruby Ruby highlight={4} theme={null}
      response = courier.send_.message(
        message: {
          to: {user_id: "user_123"},
          routing: {method: "single", channels: ["slack"]},
          template: "nt_01kx4h2jdafq8bk9aftxak4b40"
        }
      )
      ```

      ```go Go highlight={8} 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")},
      		},
      		Routing: courier.SendMessageParamsMessageRouting{
      			Method:   string(shared.MessageRoutingMethodSingle),
      			Channels: []shared.MessageRoutingChannelUnionParam{{OfString: courier.String("slack")}},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      	},
      })
      ```

      ```java Java highlight={6} theme={null}
      SendMessageParams.Message message = SendMessageParams.Message.builder()
          .to(SendMessageParams.Message.To.ofUserRecipient(
              UserRecipient.builder().userId("user_123").build()))
          .routing(SendMessageParams.Message.Routing.builder()
              .method(SendMessageParams.Message.Routing.Method.SINGLE)
              .channels(List.of(MessageRoutingChannel.ofString("slack")))
              .build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .build();

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

      ```php PHP highlight={4} theme={null}
      $response = $client->send->message(
        message: [
          'to' => ['user_id' => 'user_123'],
          'routing' => ['method' => 'single', 'channels' => ['slack']],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
        ],
      );
      ```

      ```csharp C# highlight={6} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Routing = new() { Method = "single", Channels = ["slack"] },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      };

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

      ```bash CLI highlight={4} theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"user_id": "user_123"}' \
        --message.routing '{"method": "single", "channels": ["slack"]}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

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

    Slack content is built in the template's Slack block rather than in the send, which is why this call carries a template id and no inline content.
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Check Slack">
    The message arrives as a direct message from your app, not from a person.
  </Step>

  <Step title="Read the status">
    Open <AppLink href="https://app.courier.com/logs">Logs</AppLink>. The provider response carries Slack's own error when something fails, which is more specific than the Courier status alone.
  </Step>
</Steps>

Two failures account for most first attempts. **`not_in_channel`** means the app is not in the channel you targeted, so invite it. **`users_not_found`** means the email on the profile does not match any member of that workspace, which is the usual cost of targeting by email.

## What changes in production

**A token per workspace, not per app.** Each customer installing your Slack app produces its own token. Store it on the profiles belonging to that customer, or on a <Doc href="/docs/tenants/overview">tenant</Doc> if you scope by customer already.

**Tokens can be revoked.** An admin removing the app invalidates its token, and sends then fail per recipient rather than globally. Watch for `invalid_auth` in the logs and prompt a reinstall.

**Slack rate limits per app.** Its limits apply to your app rather than to Courier, so a high-volume broadcast into one workspace is the case to watch.
