Platforms
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 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 |
| Thread replies | Yes, with threadTs |
Post types |
| Files | Up to 10 per post | Media requirements |
| Link and media unfurling | On or off per post | Platform fields |
| Custom name and avatar | Per post or per account | Message identity |
| Scheduling | Yes | Scheduling |
| Edit and delete published messages | Yes | Editing |
| DMs and mentions in the inbox | Yes | Inbox |
| Start a DM with a member | Yes | 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 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.
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 with the channelId (or several in channelIds) and the pendingDataToken:
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 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
/invitein 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.
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
}'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);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; 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 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; replyTo threads the reply. Reactions use Add reaction with a named Slack emoji such as :thumbsup:.
To start a DM, find the member with List Slack workspace members and pass their member id as participantId to Create conversation:
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." }'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);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, sequences) run on DMs only. For channels, react to
message.receivedwith 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 covers the error format.
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/connect/slack |
List Slack channels for the channel picker |
POST /v1/connect/slack |
Connect a Slack channel |
GET /v1/accounts/{accountId}/slack-settings |
Get Slack account settings |
PATCH /v1/accounts/{accountId}/slack-settings |
Update Slack account settings |
GET /v1/accounts/{accountId}/slack-members |
List Slack workspace members |
POST /v1/posts |
Create post |
POST /v1/posts/{postId}/edit |
Edit published post |
GET /v1/inbox/conversations |
List conversations |
POST /v1/inbox/conversations |
Create conversation |
POST /v1/inbox/conversations/{conversationId}/messages |
Send message |
POST /v1/inbox/conversations/{conversationId}/messages/{messageId}/reactions |
Add reaction |
#Webhook events
| Event | When it fires |
|---|---|
post.published |
Post published. |
post.failed |
Post failed. |
message.received |
Message received. |
conversation.started |
Conversation started. |
message.sent |
Message sent. |
reaction.received |
Reaction received. |