Skip to content

Field notes

Slack API fix guides

Slack answers almost everything with HTTP 200, and puts the failure in the body as ok: false. Code that checks the status code sees success and moves on, so a bot that is not in the channel, or a token missing one scope, looks exactly like a bot that worked. Each note here explains one such problem and gives you a script that finds it through the API.

Read-only tokenPython and Node.jsTests included
Browse all 108 scripts on GitHub Download them all as a zip Field guide PDF (108 fixes, 179 KB) Follow @allanninal Every fix on this page has a tested Python and Node.js script in the repo. Free, and MIT as stated in its README.
Why these scripts never write

A script here holds a token that can post into your workspace and read your conversations. So these read, they tell you exactly what is wrong, and they print the repair: the scope to add, the channel to join, the reinstall URL. You run it.

Slack is unusually good at saying what is missing — a missing_scope error names both what was needed and what was provided — and most of these notes are about reading the answer it already gave you.

Slack

SlackDiagnostic

Slack answers HTTP 200 and puts the failure in the body

Every Slack Web API error arrives as 200 OK with ok: false in the JSON. Clients that check the status code read every failure as a success.

SlackDiagnostic

Not_in_channel: the bot was never invited to the channel

Installing a Slack app joins it to nothing. The token is valid, the channel ID is right, and every call returns 200 with ok: false and not_in_channel.

SlackDiagnostic

Missing_scope tells you the scope needed and the ones you have

Slack names both needed and provided on a missing_scope error, and X-OAuth-Scopes lists the whole grant. Adding the scope is not enough: reinstall.

SlackDiagnostic

Next_cursor is ignored so only the first page is ever seen

A Slack list call returns 100 items and a cursor. Code that reads the array and stops loses the rest silently, with ok: true and no error anywhere.

SlackDiagnostic

Admin.* needs a user token your install never stored

admin.*:read cannot be granted to a bot token at all. Inventory the credentials you hold and find the missing half of the OAuth grant before you call anything.

SlackDiagnostic

Not_an_admin: workspace admin is not the org role

The token class is right and the human is not. users.info returns two different is_admin fields, and admin.* checks the one under enterprise_user.

SlackDiagnostic

Feature_not_enabled: the admin API needs Enterprise

Nobody is wrong here: the customer is not on a plan that has the admin surface. Measure capability per tenant and never store an unknown as an absence.

SlackDiagnostic

Approved in 37 workspaces and restricted in three

An app can sit on the approved list in one workspace and the restricted list in another. Map your own app_id across the org and read the request queue.

SlackDiagnostic

Ekm_access_denied: the customer holds the keys

Enterprise Key Management refuses the content, not the caller. Map which channels are affected with two reads each, then skip them instead of retrying.

SlackDiagnostic

The same message posted three times, and the ts says why

Slack has no idempotency key, so every duplicate is a real second call. The gap between the copies names the cause: a double subscription or a retry.

SlackDiagnostic

The bot answers its own messages in an endless loop

message.channels delivers your own posts back to you. Without a bot_id guard the handler replies to its own reply, and history shows the run.

SlackDiagnostic

Files made public with a link that works without a Slack login

files.list reports public_url_shared per file. Where it is true the permalink_public opens for anyone on the internet, with no login and no expiry.

SlackDiagnostic

Conversations.history clamped to 15 objects and 1 per minute

Ask for 200 messages and get 15. Slack clamps conversations.history for apps that are not on the Marketplace, and there is no setting to change.

SlackDiagnostic

Installs keyed on team_id alone collide on Enterprise Grid

On Grid an install is identified by enterprise_id, team_id and is_enterprise_install. A store keyed on team_id alone hands one tenant another's token.

SlackDiagnostic

Files.upload is retired: one probe returns method_deprecated

files.upload was sunset for every app on 12 November 2025. One argument-free read of the method answers method_deprecated and settles it in a second.

SlackDiagnostic

Every Slack profile has a null email and nothing errored

users:read returns profiles with no email at all. The address needs users:read.email, a separate scope, and its absence is a missing key rather than an error.

SlackDiagnostic

Slack disabled event delivery and will not turn it back on

Fail more than 95% of deliveries in an hour and Slack switches your event subscriptions off. Recovery is manual, and the Web API never reports the flag.

SlackDiagnostic

The scope was granted to the user token, not the bot

Slack keeps two scope lists per app. Add the scope to one and call with the other and missing_scope survives every reinstall you throw at it.

SlackDiagnostic

Token_revoked: the app is gone and retrying will not help

token_revoked is permanent. Sweep auth.test across the installation store, sort the dead rows by what should happen to them, and stop scheduling work.

SlackDiagnostic

Account_inactive: the installer left and took the token

A user token dies with its human. Join your install rows against the member directory to find the automations that are one offboarding away from stopping.

SlackDiagnostic

Token_expired every 12 hours because rotation is on

Rotated Slack tokens live 43200 seconds. Read the xoxe. prefix and the stored expires_in to find installs with no refresh loop, before tonight breaks them.

SlackDiagnostic

Slack refresh tokens are single use: a replay kills the pair

Two replicas refreshing a rotating Slack token at once burn a single-use refresh token. The evidence is in your refresh ledger, not in the API.

SlackDiagnostic

Invalid_auth: the xapp- token is in the Web API slot

Slack issues six token classes with different prefixes. A prefix check across your environment finds the swap before a single request is sent.

SlackDiagnostic

Not_allowed_token_type: right secret, wrong token class

The token authenticates everywhere else and one method still refuses it. Probe each method and read an argument error as proof the class was accepted.

SlackDiagnostic

The token holds admin scopes the app has never called

Nothing is failing, which is the problem. Compare X-OAuth-Scopes against the methods your code actually calls and prune what has no call site.

SlackDiagnostic

Classic Slack app: the scope list says bot, client, read

A pre-2020 Slack app holds coarse scopes no reinstall can change. Read X-OAuth-Scopes and the vocabulary alone tells you which era you are in.

SlackDiagnostic

Socket Mode never connects: the xapp- token is underscoped

apps.connections.open is a write, so a read-only script proves Socket Mode viability through the one read method an app-level token can call.

SlackDiagnostic

One event, many installs: authorizations:read fans it out

A distributed Slack app serves one workspace per event: the authorizations array is truncated, and only apps.event.authorizations.list expands it.

SlackDiagnostic

The Slack app configuration token died twelve hours ago

App configuration tokens live 12 hours and rotate through tooling.tokens.rotate. When one dies, every manifest check must read as not assessed.

SlackDiagnostic

Channel_not_found: a channel name where an ID belongs

chat.postMessage accepts #alerts and conversations.history refuses it. Check the form of every configured channel reference before either one runs.

SlackDiagnostic

Is_archived: the target channel was archived months ago

The ID still resolves and conversations.info still answers ok. The channel is frozen, the alerts stopped, and nothing on the sending side ever raised.

SlackDiagnostic

Channel_not_found: private, and the token cannot see it

A private channel without groups:read is not missing, it is invisible. Slack says channel_not_found either way, so read the grant instead of the error.

SlackDiagnostic

Channel_not_found after a rename, or worse, the wrong room

Slack channel names are mutable and reusable. A hardcoded name either stops resolving on rename day or starts resolving to a channel nobody chose.

SlackDiagnostic

Is_private: the public channel you read was converted

An admin converted a public channel to private. The ID never changed, so nothing looks stale, and the scope that governed your reads is now the wrong one.

SlackDiagnostic

Membership lost: the bot was removed and nothing said so

Any channel member can remove your app and nobody is told. Diff the bot's conversation set against last run's snapshot instead of waiting for a send to fail.

SlackDiagnostic

Channel_not_found: the DM conversation was never opened

A user ID is not a DM. Slack opens the conversation implicitly for some recipients and not others, so audit which of yours already have a D channel.

SlackDiagnostic

DMs delivered into deactivated accounts, with ok: true

Slack keeps the user and the DM after offboarding, so sends still succeed. Join your recipient list against users.list and find the addresses nobody reads.

SlackDiagnostic

Posting_to_general_channel_denied: the default channel

Only one channel refuses you, and it may not be called general. Find the channel carrying is_general before an integration quietly defaults into it.

SlackDiagnostic

Restricted_action_read_only_channel: the channel is locked

A member with chat:write, refused anyway. is_read_only is absent on most plans, so read who has actually posted instead of trusting a missing flag.

SlackDiagnostic

Thread-only and non-threadable channels refuse your post

restricted_action_thread_only_channel, or cannot_reply_to_message. Read the channel's threading convention and match your posting mode to it first.

SlackDiagnostic

Is_ext_shared: the channel is shared with another org

Nothing errors, and that is the finding. Check is_ext_shared and every member's team_id before an internal bot posts stack traces to a vendor.

SlackDiagnostic

Ratelimited: the Retry-After header nobody read

Slack says exactly how long to wait and hand-rolled clients discard it. Parse Retry-After defensively, then replay your own retry log offline.

SlackDiagnostic

Chat.postMessage is one per second, per channel

Not a tier. chat.postMessage is roughly one message a second in one channel, so read the ts values of what you already sent and measure your own cadence.

SlackDiagnostic

A Tier 1 method polled as if it were cheap

One call throttles while everything else is fine. Slack's tiers are per method, so check the interval against the documented tier before you add backoff.

SlackDiagnostic

One quota bucket, eight workers that think they are alone

Scaling to eight workers made the job slower. Slack's quota is keyed on method, workspace and app, so replicas share one bucket and resynchronise.

SlackDiagnostic

The bot joined 4,000 channels and the nightly sweep dies

Nothing failed: the bot is a member everywhere it was invited. Count the footprint, price one inventory against Tier 2, and cut the event volume.

SlackDiagnostic

Invalid_limit: asking for more than a page can hold

limit=5000 is rejected outright and limit=1000 is often quietly cut down. Audit the page size your code asks for against what each method returns.

SlackDiagnostic

Invalid_cursor: a stored cursor replayed after expiry

A cursor is a short-lived token bound to the query that made it, not an offset. Audit the checkpoints a resumable sync persists before the restart fails.

SlackDiagnostic

Message_limit_exceeded: the workspace posting cap

Not your app's quota, the whole workspace's. Rank every sender in the busiest channels by messages per minute and find out whether it is even you.

SlackDiagnostic

The Request URL never echoed back the challenge

Slack sends one challenge before it will deliver anything, and it does not follow redirects or pass an auth wall. Read the manifest, replay the exchange.

SlackDiagnostic

Three seconds to acknowledge, and what you spend them on

Slack counts anything slower than three seconds as a failed delivery. Split the handler at the ack, price one Slack call, and see what is left.

SlackDiagnostic

One missed ack becomes four deliveries and a 429

Slack retries at 0, 60 and 300 seconds. Multiply the deliveries by the calls each one makes, hold that against the tier, and find where the loop settles.

SlackDiagnostic

The dedupe key that does not survive the retry

event_id is stable across all three retries. client_msg_id is missing from a third of your events, and a 60 second TTL expires before the last one.

SlackDiagnostic

Accesslimited: the right token from the wrong network

Nothing about the app is wrong. Grid IP allowlisting refuses the call by network origin, and two vantage points prove it before anyone touches a scope.

SlackDiagnostic

The app subscribes to no events, so nothing is delivered

The scope is not the subscription. An app with app_mentions:read and an empty bot_events list is inert, and has been since the day it was installed.

SlackDiagnostic

A subscribed event whose scope the token never received

message.groups without groups:history is never delivered and never errors. Diff the subscribed events against the grant, because Slack will not.

SlackDiagnostic

Edits, joins and deletes handled as brand new messages

A message event with a subtype is not a new message. message_changed hides the text under event.message, and joins arrive with text a handler will answer.

SlackDiagnostic

One mention, two events: app_mention and message both fire

Subscribe to app_mention and message.channels and one @bot line arrives twice. The overlap is in the subscription list, and the reply spacing proves it.

SlackDiagnostic

The Request URL verified once, on a laptop that is now closed

A URL that passed the handshake on somebody's ngrok tunnel still reads as configured. Classify the host from the manifest, without sending it any traffic.

SlackDiagnostic

Still on RTM: the retired transport and the next reinstall

rtm.connect needs the classic client scope no modern app can request. Read the grant vocabulary and the replacement configuration, never the socket.

SlackDiagnostic

Invalid_blocks: the whole message dies on one block

Slack refuses the entire blocks array with one error that names nothing. Validate the payload locally, and fetch every image URL from outside your session.

SlackDiagnostic

Msg_blocks_too_long: the digest that grew past 50

Every block is valid and the message is not. A payload built one block per item is a capacity problem, so measure the headroom before the busy day finds it.

SlackDiagnostic

Blocks with no fallback text, so notifications are blank

Nothing fails. The message posts, answers ok true and renders correctly, and the push notification, the sidebar preview and the screen reader all get nothing.

SlackDiagnostic

Block Kit field ceilings: some reject, some truncate

A section text at 3,001 characters kills the whole message. A 41,000 character text is quietly cut. Measure every field before the long stack trace arrives.

SlackDiagnostic

Too_many_attachments: the legacy array has its own ceiling

Attachments are a second surface with a second ceiling: 100, not 50. Measure it, and price the migration before it lands you on the tighter one.

SlackDiagnostic

No_text: the message that rendered to nothing

A zero row query renders an empty string, the send is refused with no_text, and the only symptom is a report that did not arrive. Guard it before you send.

SlackDiagnostic

Cannot_reply_to_message: the parent stopped taking replies

The thread_ts you stored weeks ago points at a message somebody deleted or locked. Read each stored parent once before you thread under it again.

SlackDiagnostic

A reply ts used as thread_ts, so the thread flattens

Slack does not refuse a reply's ts as a parent. It reparents your reply to the original root, answers ok, and the conversation quietly stops making sense.

SlackDiagnostic

Message_not_found: the ts no longer resolves to a message

chat.update needs an exact channel and ts pair. Find the stored ts values that no longer resolve, including the ones a JSON parser turned into a float.

SlackDiagnostic

Cant_update_message: that message is not yours to edit

A message can only be changed by the identity that wrote it. Match every message you edit against your token's bot_id and user_id before the call, not after.

SlackDiagnostic

User_not_in_channel: an ephemeral nobody ever sees

chat.postEphemeral renders into one person's view of one channel. Check the recipient can see the channel before you send, and fall back to a DM.

SlackDiagnostic

Time_in_past: the scheduled send is behind the clock

post_at is Unix seconds, in the future, and within 120 days. Audit the queue for millisecond values, timezone offsets and sends with no margin at all.

SlackDiagnostic

Scheduled messages outlive the app that queued them

chat.scheduleMessage hands the send to Slack for up to 120 days. Diff the pending queue against your own records to find the sends nothing will stop.

SlackDiagnostic

Link_shared never fires: no domain was ever registered

Unfurling has four separate preconditions and a missing one is silent. Find whether the domain, the subscription, links:read or links:write is the absent one.

SlackDiagnostic

Expired_trigger_id: the modal opened a moment too late

A trigger_id is single use and lives seconds, not minutes. Measure the wait before views.open and tell expired, exchanged and invalid triggers apart.

SlackDiagnostic

Half finished uploads leave a file id that names nothing

getUploadURLExternal hands back a file id before any bytes exist. Reconcile your upload ledger against files.info to find the sequences that stopped halfway.

SlackDiagnostic

Files that uploaded fine and were shared into no channel

files.completeUploadExternal shares only when channel_id is passed. Find every file your app owns whose shares object and legacy arrays are both empty.

SlackDiagnostic

Not_visible: the file exists and your token is not in the room

files.info answers not_visible, file_not_found, file_deleted and access_denied. Only one of those is about membership, and the API will not name the room.

SlackDiagnostic

File_deleted: the message survives and the file does not

Deleting a file does not touch messages that link to it. Harvest file references from history and your store, then count the fraction that no longer resolve.

SlackDiagnostic

Url_private without a bearer header saves the login page

An unauthenticated fetch of url_private returns HTTP 200 and Slack's sign-in page. Compare the bytes your downloader saved with the mimetype files.info gives.

SlackDiagnostic

The 1 GB file ceiling, and how much headroom is left

Slack caps one file at 1 GB and the byte transfer gives up long before that. Read the size distribution of what your app uploads, not the last failure.

SlackDiagnostic

Retention deletes the history your app treats as storage

An admin sets a policy and messages and files stop existing on a schedule. Bisect conversations.history to measure the horizon in days before it costs you.

SlackDiagnostic

Socket Mode and a Request URL both switched on

Socket Mode hides the Request URL, it does not clear it. One app configuration serves every environment, so whichever process is connected takes the events.

SlackDiagnostic

Socket Mode apps cannot be listed on the Marketplace

Distribution needs a public HTTPS Request URL. Cross the manifest against your own install store before a customer asks for the app, not after.

SlackDiagnostic

The value in SLACK_APP_TOKEN is not an app-level token

apps.connections.open takes one credential class and refuses every other. Classify the token in each slot by prefix, never by opening a connection.

SlackDiagnostic

Too_many_websockets: ten is the ceiling and ghosts fill it

Slack allows ten concurrent Socket Mode connections. Count them from the deployment, then run the observed loss fraction backwards to find the leaked ones.

SlackDiagnostic

Refresh_requested: Slack takes the socket back every few hours

Socket Mode connections are refreshed on a schedule. A client that closes before it opens loses whatever arrives in the gap, in clusters you can time.

SlackDiagnostic

Socket Mode does not load balance: replicas duplicate and drop

Several open connections means Slack picks one per payload, without a distribution guarantee. Measure how often a trigger got exactly one answer.

SlackDiagnostic

Socket Mode off, no Request URL, and nowhere to deliver

Delivery needs exactly one transport and there is no fallback between them. Count the transports in the manifest, then find the date the app went quiet.

SlackDiagnostic

Buttons render, clicks vanish: interactivity is switched off

Posting a button needs chat:write. Receiving the click needs a second switch on a second screen. Read the route, then measure one surface against the other.

SlackDiagnostic

The repo manifest, the live manifest and the granted scopes

Slack app config is editable in two places that never reconcile. Export the live manifest, diff it against the repo, then add the third list nobody checks.

SlackDiagnostic

Add to Slack works in one workspace and nowhere else

Public distribution is a separate gate with its own checklist. Read the prerequisites out of the manifest before a customer tries to install, not after.

SlackDiagnostic

Bad_redirect_uri: the callback URL is not on the allow list

Slack compares redirect_uri character for character at the exchange. Hold every deployed callback against the configured list without redeeming a code.

SlackDiagnostic

App_access_restricted: an admin blocked the app for a user

The refusal is a policy on the app and the person, not on your token or your network. Resolve the affected users and find what they have in common.

SlackDiagnostic

Messages_tab_disabled: the App Home DM surface is off

The Messages tab is off by default and a second checkbox makes it read only. Read both flags, and the message.im subscription that has to accompany them.

SlackDiagnostic

A slash command your code handles was never registered

Commands are declared in app configuration, not in code. Diff the registered set against the handlers in your source, in both directions.

SlackDiagnostic

No_service: the incoming webhook died with its install

A webhook URL keeps its shape forever and stops working in silence. Join your inventory to your install records and read the bound channel, sending nothing.

SlackDiagnostic

The webhook posts to one channel whatever the payload says

An app based webhook is bound to one channel at creation. The channel field every old blog post shows is inert, and a routing table collapses into one room.

SlackDiagnostic

Invalid_payload: the webhook body stopped being JSON

Shell interpolation puts a quote in the body and it stops parsing. Sort the captured payload locally, as bytes, without sending anything to a webhook.

SlackDiagnostic

Steps from Apps was retired and the manifest still lists it

Slack retired Steps from Apps on 26 September 2024. The workflows stopped, the events stopped, and the dead declarations are still in your live manifest.

SlackDiagnostic

Method_deprecated: still on channels.*, groups.* and im.*

The finding worth having is the method that still works and carries a warning. Sweep the legacy families for it without ever issuing a deprecated write.

SlackDiagnostic

Views.publish succeeds and the Home tab is switched off

Publishing to a surface nobody can reach returns ok: true. Read the switch, the app_home_opened subscription, and which members have ever been published to.

SlackDiagnostic

The install store keeps rows for uninstalled workspaces

The finding is a row in your database, not an error from Slack. Sweep the store with auth.test, size the dead fraction, and watch whether it grows.

SlackDiagnostic

Team_access_not_granted: the token reaches one workspace

On Grid an install is org-wide or bound to one workspace. Read auth.test for the shape, probe each sibling, and separate a refusal from an invisibility.

SlackDiagnostic

Is_enterprise_install: one row for forty workspaces

An org-wide install is one grant covering every workspace. Enumerate the organisation, simulate your lookup, and count the workspaces that resolve to nothing.

SlackDiagnostic

Enterprise_is_restricted: the method is barred on Grid

The refusal is attached to the method, not to your token or your user. Inventory the calls your app makes, probe the reads, map each to its org equivalent.

SlackDiagnostic

Org_login_required: hold the installation, do not retire it

The workspace is migrating and will not answer until it finishes. Turn the error into a disposition, suspend the installation, and keep the cleanup away.

SlackDiagnostic

Team_added_to_org: user ids are renumbered by the migration

The window is intermittent, so one health check proves nothing, and when it closes your cached U ids have W equivalents. Audit them with migration.exchange.

Something not covered here?

These are the ones I keep hitting. If yours is broken in a way none of them describes, tell me on LinkedIn — it is usually how the next note gets written.