# Slack

Install the app in a Slack workspace, publish channel messages and thread replies with files, set the message identity, and handle DMs and mentions in the inbox.

Slack publishing runs through a hosted app. A workspace member installs it with OAuth, picks a channel, and that channel becomes an account you post to with `POST /v1/posts` using the platform id `slack`. You do not create a Slack app, host a bot or handle tokens. The same account receives DMs and mentions in the [Inbox](https://pozy.ai/docs/inbox) and can start a DM with any member of the workspace.

## What you can do

| Feature | Supported | Where |
| --- | --- | --- |
| Channel messages in Slack `mrkdwn` | Yes | [Post types](#post-types) |
| Thread replies | Yes, with `threadTs` | [Post types](#post-types) |
| Files | Up to 10 per post | [Media requirements](#media-requirements) |
| Link and media unfurling | On or off per post | [Platform fields](#platform-fields) |
| Custom name and avatar | Per post or per account | [Message identity](#message-identity) |
| Scheduling | Yes | [Scheduling](https://pozy.ai/docs/publishing/scheduling) |
| Edit and delete published messages | Yes | [Editing](https://pozy.ai/docs/publishing/editing) |
| DMs and mentions in the inbox | Yes | [Inbox](#inbox) |
| Start a DM with a member | Yes | [Inbox](#inbox) |
| Analytics | No | Slack has no message analytics API |

## Connect

Slack installs into a workspace, not a channel, so the flow ends with a channel choice. One connected account is one channel. To post in several channels, connect each as its own account.

### Start OAuth

Call [List Slack channels](https://pozy.ai/docs/api/listSlackChannels) with only `profileId` (and your `redirect_url`). With no `pendingDataToken` or `accountId`, it returns `authUrl` and `state`. Send the user to `authUrl` to approve the app.

```bash
curl "https://api.pozy.ai/v1/connect/slack?profileId=66a1f0c2a4b9d3e8f1a2b3c4&redirect_url=https://app.example.com/connected" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

### Pick the channel

Pozy shows a channel picker by default. In headless mode (`headless=true`), the redirect back to you carries a `pendingDataToken`. Call the same endpoint with it to list the workspace's channels yourself: public and private, archived ones left out, up to 2,000. `isMember` says whether the app is already in a channel.

### Save it

Call [Connect a Slack channel](https://pozy.ai/docs/api/connectSlackChannel) with the `channelId` (or several in `channelIds`) and the `pendingDataToken`:

```bash
curl -X POST https://api.pozy.ai/v1/connect/slack \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "profileId": "66a1f0c2a4b9d3e8f1a2b3c4", "pendingDataToken": "'"$PENDING_DATA_TOKEN"'", "channelId": "C07ABCDEF12" }'
```

The response carries the new `accountId`. The channel now shows up in [List accounts](https://pozy.ai/docs/api/listAccounts) for that profile.

To add another channel from the same workspace, skip OAuth: call List Slack channels with an existing Slack `accountId`, then save with `accountId` instead of `pendingDataToken`.

Things that trip people up:

- **Private channels.** The app cannot join a private channel on its own. Someone in the channel must invite it first (type `/invite` in the channel and choose the app). Until then the channel does not appear in the list. Public channels are joined automatically.
- **Admin approval.** Some workspaces need an admin to approve new apps, so Slack may show a "request approval" screen.
- **Unlisted app warning.** Slack may say the app "has not been reviewed by Slack" during install. That is expected for apps that are not in the Slack Marketplace.

## Post types

A post with `content` becomes a message in the account's channel. There is no `channelId` field on a Slack post; sending one returns `400`. To post somewhere else, connect that channel and use its `accountId`.

```bash cURL
curl -X POST https://api.pozy.ai/v1/posts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "*Deploy finished* for `api` v2.4.1. Release notes: <https://example.com/notes|read here>",
    "platforms": [{
      "platform": "slack",
      "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
      "platformSpecificData": { "unfurlLinks": false, "username": "Release Bot" }
    }],
    "publishNow": true
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "*Deploy finished* for `api` v2.4.1. Release notes: <https://example.com/notes|read here>",
    platforms: [{
      platform: "slack",
      accountId: "66b2e19d8c3f5a7e9d0b1c2d",
      platformSpecificData: { unfurlLinks: false, username: "Release Bot" },
    }],
    publishNow: true,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.post.platforms[0].platformPostId);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "content": "*Deploy finished* for `api` v2.4.1. Release notes: <https://example.com/notes|read here>",
        "platforms": [{
            "platform": "slack",
            "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
            "platformSpecificData": {"unfurlLinks": False, "username": "Release Bot"},
        }],
        "publishNow": True,
    },
)
print(resp.json()["post"]["platforms"][0]["platformPostId"])
```

To reply inside a thread, pass the parent message's timestamp as `threadTs`, for example `"threadTs": "1503435956.000247"`. Scheduling happens in Pozy; Slack's own scheduler is not used.

### Platform fields

| Field | Type | Notes |
| --- | --- | --- |
| `threadTs` | string | Post as a reply in this thread |
| `unfurlLinks` | boolean, default `true` | Expand links into preview cards |
| `unfurlMedia` | boolean, default `true` | Expand media links into inline previews |
| `username` | string | Display name for this message |
| `iconUrl` | URL | Avatar for this message |

### Message identity

`username` and `iconUrl` change the name and avatar on one message. Set a default for every post from a channel with [Update Slack account settings](https://pozy.ai/docs/api/updateSlackSettings); the per-post fields still win. The override is cosmetic: Slack still shows an APP badge, and the app's own name stays in the sidebar.

## Media requirements

Up to 10 files per post, uploaded through Slack's file API with the text as the caption. Pozy does not compress or convert files for Slack, and there is no format allowlist: the bytes behind your URL reach Slack as they are.

Whether an upload succeeds depends on the workspace's upload policy and, on Slack's free plan, its storage cap. A file Slack refuses fails the post with Slack's message. Files are named from the media item: `type: "video"` uploads as `.mp4`, `type: "gif"` as `.gif`, anything else from `filename`. [Media](https://pozy.ai/docs/publishing/media) hosts files up to 5 GB.

## Limits

| Limit | Value |
| --- | --- |
| Message content | 40,000 characters. Slack truncates beyond this without an error |
| Editable content | 4,000 characters. Longer posts cannot be edited |
| Files | 10 per post |
| Channels per account | 1 |

## Analytics

Slack has no public message analytics API, so Slack has no analytics in Pozy.

## Inbox

Slack events arrive in real time. The inbox takes in:

- DMs to the app
- Channel messages that @-mention the app
- Replies in threads the app is part of, including under posts you published
- Emoji reactions, as `reaction.received`

Ordinary channel conversation is not stored. The inbox is not a mirror of the workspace.

Each incoming message fires `message.received`, with `metadata.threadTs` when it is a thread reply and `metadata.mentionsBot: true` when it mentions the app. That pair is enough for the usual agent rule: always answer DMs, answer in a channel only when mentioned, reply in the thread.

Reply with [Send message](https://pozy.ai/docs/api/sendInboxMessage); `replyTo` threads the reply. Reactions use [Add reaction](https://pozy.ai/docs/api/addMessageReaction) with a named Slack emoji such as `:thumbsup:`.

To start a DM, find the member with [List Slack workspace members](https://pozy.ai/docs/api/listSlackMembers) and pass their member id as `participantId` to [Create conversation](https://pozy.ai/docs/api/createInboxConversation):

```bash cURL
curl "https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/slack-members?query=maria" \
  -H "Authorization: Bearer $POZY_API_KEY"

curl -X POST https://api.pozy.ai/v1/inbox/conversations \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "participantId": "U04ABCDE123", "message": "Hi Maria, your report is ready." }'
```
```ts Node
const membersRes = await fetch("https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/slack-members?query=maria", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const members = await membersRes.json();
if (!membersRes.ok) throw new Error(members.error);

const convoRes = await fetch("https://api.pozy.ai/v1/inbox/conversations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ accountId: "66b2e19d8c3f5a7e9d0b1c2d", participantId: "U04ABCDE123", message: "Hi Maria, your report is ready." }),
});
const convo = await convoRes.json();
if (!convoRes.ok) throw new Error(convo.error);
console.log(convo.data.conversationId);
```
```python Python
headers = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
members = requests.get(
    "https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/slack-members",
    headers=headers, params={"query": "maria"},
).json()

convo = requests.post(
    "https://api.pozy.ai/v1/inbox/conversations",
    headers=headers,
    json={"accountId": "66b2e19d8c3f5a7e9d0b1c2d", "participantId": "U04ABCDE123", "message": "Hi Maria, your report is ready."},
).json()
print(convo["data"]["conversationId"])
```

Bots, deactivated members and Slackbot are left out of the member lookup.

Constraints to plan for:

- No history backfill. The inbox starts when the channel connects.
- Messages from other bots appear for context but never trigger automations or webhooks.
- Attachments appear as links that open in Slack.
- People who DM the app become contacts; people talking in a channel do not. Built-in automations ([workflows](https://pozy.ai/docs/automations/workflows), [sequences](https://pozy.ai/docs/automations/sequences)) run on DMs only. For channels, react to `message.received` with your own logic.

## Errors you may see

| Error | Cause | Fix |
| --- | --- | --- |
| `400` with `channelId` on a post | Slack posts have no `channelId` field | Connect the channel and use its `accountId` |
| Private channel missing from the list | The app has not been invited | Invite the app in that channel, then list again |
| `missing_scope` on inbound messages | The workspace was connected before the inbox existed | Reconnect the workspace once so it grants the new scopes |
| Edit rejected | The post is over 4,000 characters | Publish a new message |
| Upload refused | The workspace's upload policy or storage cap | Ask the workspace admin, or send a link instead |
| `400` on a reaction | The emoji has no Slack name | Use a named emoji such as `:thumbsup:` |

[Errors](https://pozy.ai/docs/getting-started/errors) covers the error format.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/slack`](https://pozy.ai/docs/api/listSlackChannels) | List Slack channels for the channel picker |
| [`POST /v1/connect/slack`](https://pozy.ai/docs/api/connectSlackChannel) | Connect a Slack channel |
| [`GET /v1/accounts/{accountId}/slack-settings`](https://pozy.ai/docs/api/getSlackSettings) | Get Slack account settings |
| [`PATCH /v1/accounts/{accountId}/slack-settings`](https://pozy.ai/docs/api/updateSlackSettings) | Update Slack account settings |
| [`GET /v1/accounts/{accountId}/slack-members`](https://pozy.ai/docs/api/listSlackMembers) | List Slack workspace members |
| [`POST /v1/posts`](https://pozy.ai/docs/api/createPost) | Create post |
| [`POST /v1/posts/{postId}/edit`](https://pozy.ai/docs/api/editPost) | Edit published post |
| [`GET /v1/inbox/conversations`](https://pozy.ai/docs/api/listInboxConversations) | List conversations |
| [`POST /v1/inbox/conversations`](https://pozy.ai/docs/api/createInboxConversation) | Create conversation |
| [`POST /v1/inbox/conversations/{conversationId}/messages`](https://pozy.ai/docs/api/sendInboxMessage) | Send message |
| [`POST /v1/inbox/conversations/{conversationId}/messages/{messageId}/reactions`](https://pozy.ai/docs/api/addMessageReaction) | Add reaction |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`post.published`](https://pozy.ai/docs/webhooks/events#post-published) | Post published. |
| [`post.failed`](https://pozy.ai/docs/webhooks/events#post-failed) | Post failed. |
| [`message.received`](https://pozy.ai/docs/webhooks/events#message-received) | Message received. |
| [`conversation.started`](https://pozy.ai/docs/webhooks/events#conversation-started) | Conversation started. |
| [`message.sent`](https://pozy.ai/docs/webhooks/events#message-sent) | Message sent. |
| [`reaction.received`](https://pozy.ai/docs/webhooks/events#reaction-received) | Reaction received. |
