Zernio
Zernio
Overview

Social Posting

XInstagramFacebookLinkedInTikTokYouTubePinterestRedditBlueskyThreadsGoogle Business ProfileSnapchat

Messaging

WhatsAppTelegramDiscordSlack

Telephony

Phone NumbersVoice & CallsSMS

Advertising

Meta AdsGoogle AdsLinkedIn AdsTikTok AdsPinterest AdsX AdsOpenAI Ads

Commerce

Shopify
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources

Slack

Publish messages and thread replies to a Slack channel with the Zernio API, with file uploads, a per-message bot identity, and an inbox that receives DMs and mentions in realtime.


Publish messages and thread replies to a Slack channel with POST /v1/posts and platform: "slack". The same account receives DMs and bot mentions in the inbox, and can start a DM with any member of its workspace.

Quick reference

PropertyValue
Content limit40,000 characters (Slack truncates silently beyond this)
Edit limit4,000 characters (longer posts cannot be edited)
Media per postUp to 10 files
FormattingSlack mrkdwn
Post typesChannel messages, thread replies
SchedulingYes (Zernio-side; Slack's native scheduler is not used)
ThreadsYes (threadTs)
Link unfurlingConfigurable per post
Per-message identityYes (display name and avatar override)
Inbox (DMs)Yes
Start a conversationYes (DM any member of the workspace)
Inbox (channel messages)Mentions and threads only, not all chatter
Contacts and built-in automationsDMs only
AnalyticsNo (Slack has no public message analytics API)

Before you start

Slack requires the Zernio bot app installed in the workspace. You do not create your own Slack app, host a bot or manage tokens: the user installs Zernio's app through OAuth and you post through the API.

One connected account is one channel. To publish to several channels, connect each one as its own account. Public channels are joined automatically on connect. A private channel needs a member to run /invite @Zernio in it first, because the bot cannot join a private channel on its own, and until then it does not appear in the channel list.

Some workspaces require admin approval for new apps, so Slack may show a "request approval" screen, and a workspace can be configured to allow only apps listed in the Slack Marketplace. Zernio's app is distributed but not yet listed, so Slack says it "has not been reviewed by Slack" during install. That is expected.

Connect

Call GET /v1/connect/slack with profileId on List Slack channels. With no pendingDataToken or accountId it returns the authUrl and state that start OAuth, like GET /v1/connect/{platform} does for other platforms. Slack installs the bot into a workspace, not a channel, so the flow ends with a channel choice:

  1. Send the user to authUrl; they authorize the workspace on Slack's consent screen.
  2. Zernio lists the workspace's channels and the user picks one.
  3. POST /v1/connect/slack saves the channel as an account.

In headless mode (headless=true) the post-OAuth redirect carries a pendingDataToken and connect_token. List the channels yourself:

curl "https://zernio.com/api/v1/connect/slack?profileId=66a1f0c2a4b9d3e8f1a2b3c4&pendingDataToken=$PENDING_DATA_TOKEN" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200):

{
  "team": { "id": "T0123456789", "name": "Example Co", "icon": "https://..." },
  "channels": [
    { "id": "C0123456789", "name": "marketing", "isPrivate": false, "isMember": true },
    { "id": "G0123456789", "name": "launch-room", "isPrivate": true, "isMember": false }
  ]
}

Channels are read live from Slack, public and private, archived excluded, up to 2,000. isMember says whether the bot is already in the channel. Then finalize with Connect Slack channel:

curl -X POST https://zernio.com/api/v1/connect/slack \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
    "channelId": "C0123456789",
    "pendingDataToken": "'"$PENDING_DATA_TOKEN"'"
  }'

Response (200):

{
  "message": "Slack channel connected successfully",
  "account": {
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "platform": "slack",
    "username": "marketing",
    "displayName": "marketing",
    "isActive": true,
    "channelId": "C0123456789",
    "teamId": "T0123456789",
    "teamName": "Example Co"
  }
}

account.accountId is the id every sample below uses. The channel is now an account in that profile, so it also shows up in GET /v1/accounts?profileId=66a1f0c2a4b9d3e8f1a2b3c4.

Adding another channel from the same workspace needs no second OAuth: call GET /v1/connect/slack with profileId and accountId of an existing Slack account to list the channels, then POST with accountId instead of pendingDataToken. The connecting accounts guide covers the flow and scopes in general.

OAuth scopes

Publishing and the inbox run through a single bot installation:

ScopeWhat it enables
chat:writePost messages as the bot
chat:write.publicPost to public channels the bot has not joined
chat:write.customizePer-message display name and avatar override
channels:joinJoin public channels on connect
files:writeUpload media with a post
channels:read, groups:readList channels so you can pick one
team:readRead workspace name and icon
channels:history, groups:history, im:history, mpim:historyReceive inbound messages for the inbox
im:read, mpim:read, im:writeDirect message conversations
users:readResolve sender names on inbound messages
files:readInbound message attachments

Workspaces connected before the inbox shipped must reconnect once. Existing bot tokens keep the scopes they were granted, so inbound messages fail with missing_scope until the workspace re-authorizes from the dashboard.

Publish

A plain post becomes a message in the channel the account was connected to. Fields in platformSpecificData on the Slack entry turn it into a thread reply, control unfurling and override the bot's identity.

Channel message

import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: published } = await zernio.posts.createPost({
  body: {
    content: 'Deploy finished :rocket:',
    platforms: [
      { platform: 'slack', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
    ],
    publishNow: true
  }
});

console.log(published.post.platforms[0].platformPostUrl);

Response (201):

{
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "published",
    "platforms": [
      {
        "platform": "slack",
        "status": "published",
        "platformPostUrl": "https://example-co.slack.com/archives/C0123456789/p1785320613863000"
      }
    ]
  }
}

Every sample below changes only the mediaItems or the platforms entry of this request.

There is no channelId field on a post. The channel is whichever one the account was connected to; passing channelId in platformSpecificData returns a 400. Connect the channel you want and target its accountId.

Thread reply

threadTs posts the message inside an existing thread. Pass the parent message's timestamp:

{
  "platform": "slack",
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "platformSpecificData": { "threadTs": "1785320613.863000" }
}

Files

Up to 10 files per post, uploaded through Slack's file API with the text as the caption:

"mediaItems": [
  { "type": "image", "url": "https://cdn.example.com/dashboard.png" },
  { "type": "image", "url": "https://cdn.example.com/error-rate.png" }
]

Message identity

username and iconUrl override the bot's display name and avatar for one message. The override is cosmetic: Slack still shows an APP badge, and the app's identity in the sidebar stays "Zernio". Set a default identity for every post from the channel with Slack account settings; the per-post fields still win.

{
  "platform": "slack",
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "platformSpecificData": {
    "unfurlLinks": false,
    "username": "Status Bot",
    "iconUrl": "https://example.com/status.png"
  }
}

Edit and delete

Edit post replaces the message text up to 4,000 characters; media cannot be swapped. Longer messages cannot be edited, because Slack's edit limit is stricter than its posting limit. There is no time window unless workspace admins restrict message editing. Unpublishing deletes the Slack message; workspace retention or compliance settings can block the deletion, which is an admin setting, not a Zernio error. A media post occasionally cannot be edited or deleted when Slack returned no message reference at publish time.

Platform fields

All fields go in platformSpecificData on the Slack entry.

FieldTypeDefaultDescription
threadTsstringPost as a reply inside an existing thread. The parent message's timestamp, for example "1503435956.000247".
unfurlLinksbooleantrueExpand links in the text into preview cards.
unfurlMediabooleantrueExpand media links into inline previews.
usernamestring(account default)Override the bot's display name for this message.
iconUrlstring(account default)Override the bot's avatar for this message.

Media requirements

Up to 10 files per post, uploaded through Slack's file API with the text as the caption. Slack is the one platform Zernio neither compresses nor transcodes for, and it applies no format allowlist here: the bytes behind your URL reach Slack as they are, whatever the type. The per-file ceiling is therefore Slack's own, and the workspace's upload policy and, on Slack's free plan, its storage cap decide whether an upload succeeds; a file Slack refuses fails the post with Slack's message. Zernio names the upload from the media item, type: "video" as .mp4 and type: "gif" as .gif, and otherwise from filename. Host files on a public URL or through the media endpoint, which caps a single file at 5 GB.

Analytics

Slack has no public message analytics API, so GET /v1/analytics?platform=slack reports zero for every Slack post. Reactions and thread replies reach you through the inbox instead.

Inbox

Slack supports DMs and channel mentions in the inbox, delivered in realtime through Slack's Events API. Zernio ingests:

  • DMs to the Zernio bot
  • Channel messages that @-mention the bot
  • Replies in threads the bot is already part of, including under a post Zernio published
  • The bot's own messages
  • Emoji reactions added to or removed from a message, as reaction.received

Ordinary channel conversation is deliberately not ingested. The inbox is not a mirror of the workspace, so day-to-day chatter in a connected channel is never stored.

Every incoming message fires the message.received event. Its metadata carries:

FieldMeaning
threadTsPresent when the message is a thread reply
mentionsBottrue when the message @-mentions the Zernio bot

That pair covers the usual agent rule: answer when mentioned in a channel, always answer DMs, and reply inside the thread. Reply from the inbox or with Send inbox message on POST /v1/inbox/conversations/{conversationId}/messages; the message goes out as the bot, and replyTo threads the reply. POST /v1/inbox/conversations/{conversationId}/messages/{messageId}/reactions reacts to a message; the emoji has to have a Slack name, such as :thumbsup:, and an unnamed character returns a 400.

Constraints:

  • No history backfill. The inbox starts at connect time, a Slack restriction for apps not listed in the Marketplace.
  • Messages from other bots appear for context but never trigger automations or webhook events (loop guard).
  • Attachments appear as links that open in Slack.
  • Contacts and the built-in automations (sequences, workflows) are DM-only. Someone who DMs the bot becomes a contact; people talking in a connected channel do not, otherwise every colleague in the channel would be added as a contact. Automate channels with your own agent reacting to message.received.

Start a conversation

You do not have to wait for someone to message the bot first. Look up a workspace member with List Slack members, then open the DM by passing their member id as participantId to Create inbox conversation:

const { data: members } = await zernio.slack.listSlackMembers({
  path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
  query: { query: 'maria' }
});

const { data: conversation } = await zernio.messages.createInboxConversation({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    participantId: members.members[0].id,
    message: 'Hi Maria, the campaign is live.'
  }
});

console.log(conversation.data.conversationId);

Response (200) from the member lookup:

{
  "members": [
    { "id": "U01ABCDEF", "name": "Maria Lopez", "username": "maria", "picture": "https://..." }
  ]
}

Response (201) from the conversation call:

{
  "success": true,
  "data": {
    "messageId": "1785320613.863000",
    "conversationId": "66c3d2ae7b4f6c8d0e1f2a3b",
    "participantId": "U01ABCDEF",
    "participantName": "Maria Lopez"
  }
}

Slack addresses people by opaque id, never by name. Bots, deactivated members and Slackbot are left out of the lookup because they cannot hold a DM conversation. The DM comes from the workspace's connected bot, so it reaches members of that workspace only, and the reply lands in the same conversation.

What you cannot do

Slack's bot API does not expose:

  • Messaging people outside the connected workspace
  • Posting to several channels from one connected account (connect each channel separately)
  • Choosing the channel per post (channelId in platformSpecificData returns a 400)
  • Engagement metrics
  • Message history from before the connection
  • Joining a private channel without being invited

Common errors

ErrorCauseFix
Bot is not a member of this channelThe bot was removed, or the channel is privateRun /invite @Zernio in the channel.
Channel was not found or the bot lost accessThe channel was deleted or the bot lost visibilityReconnect, or connect another channel.
A workspace setting prevents postingA Slack admin restricted posting in that channelOnly a Slack admin can lift it.
File uploads are disabled or too largeThe workspace's upload policy or its free-plan storage capCheck the workspace's upload policy and storage.
Workspace access was revokedThe app was uninstalled from SlackReconnect from the dashboard.
missing_scope on inbound messagesThe workspace was connected before the inbox shippedReconnect once.

A publishNow: true post that Slack rejects returns 207 with post.status: "failed" and the message in platforms[].errorMessage:

{
  "message": "Post created but publishing failed",
  "error": "All platforms failed",
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "failed",
    "platforms": [
      {
        "platform": "slack",
        "status": "failed",
        "errorMessage": "Bot is not a member of this channel"
      }
    ]
  }
}

207 is a 2xx status, so fetch(...).ok is true; branch on the status code and on post.status. Error handling covers the envelope.

Disconnecting one channel account does not revoke the workspace token, so sibling channel accounts keep working. Uninstalling the Zernio app from Slack disconnects every channel account of that workspace in Zernio automatically.

Related

  • Connecting accounts: the OAuth flow in general.
  • Create post and Edit post: every field of the request.
  • Slack account settings: the default message identity for a channel.
  • Messages: the inbox API.
  • Account health: verify the connection.
  • Pricing: what the inbox and outbound messages cost.
Was this page helpful?

Discord

Publish messages, embeds, polls, forum posts and threads to Discord channels with the Zernio API, send DMs, manage roles and pins, and create scheduled events, with no bot hosting.

Phone Numbers

Buy or port in a real phone number with the Zernio API, then turn on Calls, SMS and WhatsApp on that same number.

On this page

Quick referenceBefore you startConnectOAuth scopesPublishChannel messageThread replyFilesMessage identityEdit and deletePlatform fieldsMedia requirementsAnalyticsInboxStart a conversationWhat you cannot doCommon errorsRelated