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