Diagnostic Slack
not_allowed_token_type: right secret, wrong token class
{"ok": false, "error": "not_allowed_token_type"}. The token works. It authenticates, it has scopes, it calls a dozen other methods happily. This one method, and only this one, will not take it — and the error names the problem without naming the solution, because it says what is wrong with the class you brought and nothing about which class it wanted.
Some Slack methods accept exactly one class of credential. admin.* wants a user token held by an org owner or admin and rejects a bot token outright. apps.connections.open and apps.event.authorizations.list want an app-level xapp- token, sent in the Authorization header rather than as a form field. Everything else wants xoxb- or xoxp-.
So the diagnosis is per method, not per token. Establish what class you hold with auth.test — a bot_id in the response means a bot token, its absence means a user token — then probe each method you depend on and read the error. not_allowed_token_type is the finding. missing_scope is emphatically not: it means the class was accepted and the grant was short. And an error about the arguments is the best news of all, because a method only gets as far as validating arguments once it has accepted the credential.
The problem in plain words
This is the error that arrives after you have already fixed the obvious things. The token is not expired. It is not the wrong prefix — a bot token is a perfectly good Slack credential and it is in the variable it belongs in. Twelve methods accept it. The thirteenth does not, and the message is a bare statement that this class of token is not allowed here.
What it will not tell you is which class is allowed. That fact lives on the method's reference page, in a line most people scroll past, and it varies in ways that do not follow an obvious rule. admin.teams.list reads a list of workspaces and needs a human org admin's token, because Grid administration is modelled as something a person does. apps.event.authorizations.list reads which installations an event was delivered for, and needs the app's own token, because it is a fact about the app rather than about a workspace.
The second half of the trap is transport. The app-level methods want the token in the Authorization header. Send the same correct xapp- token as a form parameter, the way older Slack examples pass token=, and you get the identical not_allowed_token_type — with the right credential, in the right variable, for the right method. Nothing about the error hints that the problem is where you put it.
Why it happens
Class is a property of the method, not of the app. One app routinely holds three credentials: a bot token for the Web API, an app-level token for Socket Mode, and possibly a user token for anything that must act as a person. Which one to send is decided per call site, and a client that has a single configured token cannot express that.
admin.* is a person's authority, not an app's. These methods reject bot tokens categorically. The user token has to belong to an org owner or admin on Enterprise Grid, and it needs the matching admin.*:read scope on top. A bot cannot be granted the authority because the authority is modelled as belonging to a human.
An argument error means the class was accepted. If a method answers invalid_arguments or names a missing parameter, it has already checked and approved the credential. That is the cleanest possible confirmation that the class is right, and it is the one signal in this audit that is good news wearing an error's clothing.
missing_scope belongs to a different note. It means the class was fine and the grant was short, and the repair is a scope plus a reinstall rather than a different credential. An audit that lumps the two together sends people to the wrong configuration page, which is the specific failure this script exists to prevent.
The header is part of the contract. For the app-level methods, Authorization: Bearer xapp-... is the supported placement. A token passed as a parameter is treated as the wrong class, so "it worked in curl once" and "it fails in the client" can both be true of the same secret.
The fix, as a flow
Every probe is sent the credential its family calls for, and the answer is read as a statement about class. The unusual row is the argument error: a method checks the credential before it checks the arguments, so a complaint about arguments is a confirmation.
How to fix it
Establish the class you are holding
One auth.test. A bot_id in the response means a bot token; its absence with a user_id means a user token. Do this first, because every subsequent verdict is a comparison against it and an unauthenticated token makes the whole probe meaningless.
Find out whether that user is an admin
For a user token, users.info?user=<the user_id from auth.test> reports is_admin and is_owner. That is what separates a user token that will satisfy admin.* from one that will be refused for a reason no error message will spell out. It needs users:read, so treat it as optional enrichment.
Derive the class each method wants from its name
The families are regular enough to encode: admin. wants an org admin's user token, apps.connections. and apps.event. want an app-level token, apps.manifest. wants an app configuration token, and everything else takes a bot or user token. Deriving it means the audit covers methods you have not thought about yet.
Probe with the credential the method should get
Where the app holds several tokens, send each method the one its family calls for — the app-level token to the apps.event. probe, the user token to the admin. probes. A script that sends the bot token to everything reproduces the bug rather than diagnosing it.
Read the errors as statements about class
not_allowed_token_type: wrong class, and the table says which is right. missing_scope: right class, missing grant, different repair. invalid_auth: the credential itself is wrong and nothing about class can be concluded. An argument error: the class was accepted, which is the answer you wanted.
Route the credential at the call site, not in the client
Keep a per-family mapping from method to credential and pass the right one explicitly. In the SDKs that means a second client instance rather than swapping a token on a shared one, and for the app-level methods it means the Authorization header rather than a form field.
How to check it worked
Re-run once each method is being called with the credential its family requires. Every probe should report either allowed or class-accepted, and no probe should report a class mismatch.
python3 slack_method_token_class.py
# holding bot token B0123 in T0123
# allowed team.info
# wrong-class admin.teams.list wants org-admin-user, holding bot
# 4 method(s) probed, 1 refusing this class of token
The full code
Three pure functions carry the diagnosis and one GET per method feeds them. required_class derives what a method wants from its name, token_class reads auth.test and the optional profile to say what you hold, and verdict reads one probe's error — including the argument errors it deliberately treats as confirmation rather than failure.
"""Work out which Slack methods refuse the class of token this app deploys.
Read only. Every probe is a GET against a read method, and the arguments are
chosen so that a method which accepts the credential still cannot do anything
with the call. The repair is printed, never performed.
"""
import argparse
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("slack_method_token_class")
API = "https://slack.com/api/"
# Method families and the class of credential each one accepts. Derived from the
# name rather than listed per method, so a method nobody thought about is still
# classified correctly.
FAMILIES = (
("admin.", "org-admin-user"),
("apps.connections.", "app-level"),
("apps.event.", "app-level"),
("apps.manifest.", "app-config"),
)
# Which environment variable holds the credential for each class.
ENV_FOR_CLASS = {
"bot-or-user": "SLACK_BOT_TOKEN",
"org-admin-user": "SLACK_USER_TOKEN",
"app-level": "SLACK_APP_TOKEN",
"app-config": "SLACK_CONFIG_TOKEN",
}
# The credential was accepted and the call was refused on its arguments. That is
# the confirmation this audit is looking for, not a failure.
ARGUMENT_ERRORS = {
"invalid_arguments", "invalid_arg_name", "invalid_array_arg", "invalid_limit",
"event_context_not_found", "channel_not_found", "user_not_found",
"team_not_found", "not_found", "missing_argument",
}
CREDENTIAL_ERRORS = {
"invalid_auth", "not_authed", "token_revoked", "token_expired", "account_inactive",
}
PROBES = (
("team.info", {}),
("conversations.list", {"limit": "1", "types": "public_channel"}),
("admin.teams.list", {"limit": "1"}),
("apps.event.authorizations.list", {"event_context": "audit-probe"}),
)
def required_class(method):
"""Which class of credential a method will accept, from its name. Pure."""
for prefix, cls in FAMILIES:
if method.startswith(prefix):
return cls
return "bot-or-user"
def token_class(identity, profile=None):
"""What class the credential in hand is. Pure.
`identity` is a parsed auth.test body. `profile` is an optional users.info
body, which is the only way to learn whether a user token belongs to an
admin -- auth.test does not say.
"""
if identity.get("ok") is not True:
return ("unusable",
"auth.test answered error=%s, so nothing can be concluded about "
"class until the credential itself works."
% (identity.get("error") or "<no error field>"))
if identity.get("bot_id"):
return ("bot", "bot token %s in %s"
% (identity.get("bot_id"), identity.get("team_id")))
user = (profile or {}).get("user") or {}
if user.get("is_admin") or user.get("is_owner"):
return ("org-admin-user", "user token for %s, who is an admin or owner"
% identity.get("user_id"))
if profile:
return ("user", "user token for %s, who is neither admin nor owner. The "
"admin family will refuse it." % identity.get("user_id"))
return ("user", "user token for %s; admin status unknown without users:read"
% identity.get("user_id"))
def verdict(method, have, body):
"""Read one probe's answer as a statement about token class. Pure."""
want = required_class(method)
if body.get("ok") is True:
return ("allowed", "answered ok with a %s credential" % have)
error = body.get("error") or "<no error field>"
if error == "not_allowed_token_type":
return ("wrong-class",
"wants %s, holding %s. Send this method the credential its family "
"requires rather than the app's default token." % (want, have))
if error == "missing_scope":
return ("scope-not-class",
"the %s class was accepted and the grant was short: needed=%s. "
"That is a scope problem, not a token-class one."
% (have, body.get("needed") or "?"))
if error in CREDENTIAL_ERRORS:
return ("credential",
"error=%s. The credential itself is wrong or dead, so this probe "
"says nothing about class." % error)
if error in ARGUMENT_ERRORS:
return ("class-accepted",
"error=%s, which is a complaint about the arguments. A method only "
"validates arguments after it has accepted the credential, so the "
"%s class is right for it." % (error, have))
return ("inconclusive",
"error=%s, which is neither a class refusal nor an argument complaint. "
"Read the method reference before concluding anything." % error)
def probe(session, method, params, token):
r = session.get(API + method, params=params,
headers={"Authorization": "Bearer " + token}, timeout=30)
try:
return r.json()
except ValueError:
return {"ok": False, "error": "unparseable_body"}
def users_info(session, token, user_id):
if not user_id:
return None
body = probe(session, "users.info", {"user": user_id}, token)
return body if body.get("ok") is True else None
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--method", action="append", default=[],
help="probe this read method as well as the default set; repeatable")
args = ap.parse_args()
bot = os.environ.get("SLACK_BOT_TOKEN")
if not bot:
log.error("set SLACK_BOT_TOKEN (the token the app actually deploys with)")
return 2
s = requests.Session()
identity = probe(s, "auth.test", {}, bot)
profile = users_info(s, bot, identity.get("user_id")) if identity.get("ok") else None
have, note = token_class(identity, profile)
log.info("%-16s %s", "holding", note)
if have == "unusable":
return 2
probes = list(PROBES) + [(m, {}) for m in args.method]
bad = 0
for method, params in probes:
want = required_class(method)
env_name = ENV_FOR_CLASS.get(want, "SLACK_BOT_TOKEN")
token = os.environ.get(env_name)
if not token:
log.info("%-16s %-32s wants %s from %s, which is unset. Skipped rather "
"than probed with the wrong credential.",
"no-credential", method, want, env_name)
continue
# When a family has its own credential, report the class of that one.
holding = have if env_name == "SLACK_BOT_TOKEN" else want
state, detail = verdict(method, holding, probe(s, method, params, token))
line = "%-16s %-32s %s" % (state, method, detail)
if state in ("allowed", "class-accepted"):
log.info(line)
continue
bad += 1
log.warning(line)
if state == "wrong-class":
log.warning(" repair: %s wants a %s credential; put it in %s and route "
"this call to it", method, want, env_name)
if want == "app-level":
log.warning(" repair: send it as an Authorization header, not as a "
"form field, or the class is rejected anyway")
log.info("%d method(s) probed, %d refusing this class of token", len(probes), bad)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
/**
* Work out which Slack methods refuse the class of token this app deploys.
*
* Read only. Every probe is a GET against a read method, and the arguments are
* chosen so that a method which accepts the credential still cannot do anything
* with the call. The repair is printed, never performed.
*/
const API = 'https://slack.com/api/';
// Method families and the class of credential each one accepts. Derived from the
// name rather than listed per method, so a method nobody thought about is still
// classified correctly.
const FAMILIES = [
['admin.', 'org-admin-user'],
['apps.connections.', 'app-level'],
['apps.event.', 'app-level'],
['apps.manifest.', 'app-config'],
];
// Which environment variable holds the credential for each class.
const ENV_FOR_CLASS = {
'bot-or-user': 'SLACK_BOT_TOKEN',
'org-admin-user': 'SLACK_USER_TOKEN',
'app-level': 'SLACK_APP_TOKEN',
'app-config': 'SLACK_CONFIG_TOKEN',
};
// The credential was accepted and the call was refused on its arguments. That is
// the confirmation this audit is looking for, not a failure.
const ARGUMENT_ERRORS = new Set([
'invalid_arguments', 'invalid_arg_name', 'invalid_array_arg', 'invalid_limit',
'event_context_not_found', 'channel_not_found', 'user_not_found',
'team_not_found', 'not_found', 'missing_argument',
]);
const CREDENTIAL_ERRORS = new Set([
'invalid_auth', 'not_authed', 'token_revoked', 'token_expired', 'account_inactive',
]);
const PROBES = [
['team.info', {}],
['conversations.list', { limit: '1', types: 'public_channel' }],
['admin.teams.list', { limit: '1' }],
['apps.event.authorizations.list', { event_context: 'audit-probe' }],
];
/** Which class of credential a method will accept, from its name. Pure. */
export function requiredClass(method) {
for (const [prefix, cls] of FAMILIES) {
if (method.startsWith(prefix)) return cls;
}
return 'bot-or-user';
}
/**
* What class the credential in hand is. Pure.
* `profile` is an optional users.info body, the only way to learn whether a user
* token belongs to an admin -- auth.test does not say.
*/
export function tokenClass(identity, profile = null) {
if (identity?.ok !== true) {
return ['unusable',
`auth.test answered error=${identity?.error ?? '<no error field>'}, so ` +
'nothing can be concluded about class until the credential itself works.'];
}
if (identity.bot_id) {
return ['bot', `bot token ${identity.bot_id} in ${identity.team_id}`];
}
const user = profile?.user ?? {};
if (user.is_admin || user.is_owner) {
return ['org-admin-user',
`user token for ${identity.user_id}, who is an admin or owner`];
}
if (profile) {
return ['user',
`user token for ${identity.user_id}, who is neither admin nor owner. The ` +
'admin family will refuse it.'];
}
return ['user',
`user token for ${identity.user_id}; admin status unknown without users:read`];
}
/** Read one probe's answer as a statement about token class. Pure. */
export function verdict(method, have, body) {
const want = requiredClass(method);
if (body?.ok === true) return ['allowed', `answered ok with a ${have} credential`];
const error = body?.error ?? '<no error field>';
if (error === 'not_allowed_token_type') {
return ['wrong-class',
`wants ${want}, holding ${have}. Send this method the credential its family ` +
'requires rather than the app\'s default token.'];
}
if (error === 'missing_scope') {
return ['scope-not-class',
`the ${have} class was accepted and the grant was short: needed=` +
`${body.needed ?? '?'}. That is a scope problem, not a token-class one.`];
}
if (CREDENTIAL_ERRORS.has(error)) {
return ['credential',
`error=${error}. The credential itself is wrong or dead, so this probe says ` +
'nothing about class.'];
}
if (ARGUMENT_ERRORS.has(error)) {
return ['class-accepted',
`error=${error}, which is a complaint about the arguments. A method only ` +
`validates arguments after it has accepted the credential, so the ${have} ` +
'class is right for it.'];
}
return ['inconclusive',
`error=${error}, which is neither a class refusal nor an argument complaint. ` +
'Read the method reference before concluding anything.'];
}
async function probe(method, params, token) {
const url = new URL(API + method);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
try {
return await res.json();
} catch {
return { ok: false, error: 'unparseable_body' };
}
}
async function usersInfo(token, userId) {
if (!userId) return null;
const body = await probe('users.info', { user: userId }, token);
return body?.ok === true ? body : null;
}
async function main() {
const bot = process.env.SLACK_BOT_TOKEN;
if (!bot) {
console.error('set SLACK_BOT_TOKEN (the token the app actually deploys with)');
process.exitCode = 2;
return;
}
const args = process.argv.slice(2);
const extra = args.map((a, i) => (args[i - 1] === '--method' ? a : null)).filter(Boolean);
const identity = await probe('auth.test', {}, bot);
const profile = identity?.ok ? await usersInfo(bot, identity.user_id) : null;
const [have, note] = tokenClass(identity, profile);
console.log(`${'holding'.padEnd(16)} ${note}`);
if (have === 'unusable') {
process.exitCode = 2;
return;
}
const probes = [...PROBES, ...extra.map((m) => [m, {}])];
let bad = 0;
for (const [method, params] of probes) {
const want = requiredClass(method);
const envName = ENV_FOR_CLASS[want] ?? 'SLACK_BOT_TOKEN';
const token = process.env[envName];
if (!token) {
console.log(`${'no-credential'.padEnd(16)} ${method.padEnd(32)} wants ${want} ` +
`from ${envName}, which is unset. Skipped rather than probed with ` +
'the wrong credential.');
continue;
}
// When a family has its own credential, report the class of that one.
const holding = envName === 'SLACK_BOT_TOKEN' ? have : want;
const [state, detail] = verdict(method, holding, await probe(method, params, token));
const line = `${state.padEnd(16)} ${method.padEnd(32)} ${detail}`;
if (state === 'allowed' || state === 'class-accepted') {
console.log(line);
continue;
}
bad += 1;
console.warn(line);
if (state === 'wrong-class') {
console.warn(` repair: ${method} wants a ${want} credential; put it in ` +
`${envName} and route this call to it`);
if (want === 'app-level') {
console.warn(' repair: send it as an Authorization header, not as a form ' +
'field, or the class is rejected anyway');
}
}
}
console.log(`${probes.length} method(s) probed, ${bad} refusing this class of token`);
process.exitCode = bad ? 1 : 0;
}
// Only run when invoked directly, so importing this module in the tests does not
// execute main() and fail the file on a missing token.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
The case that carries this note is the argument error. event_context_not_found from apps.event.authorizations.list looks like a failure and is in fact the proof that the app-level token was accepted, so the tests pin it as class-accepted rather than as any flavour of problem. Alongside it, missing_scope has to stay firmly out of this note's territory.
from slack_method_token_class import required_class, token_class, verdict
def test_method_families_map_to_the_class_they_want():
assert required_class("admin.teams.list") == "org-admin-user"
assert required_class("apps.connections.open") == "app-level"
assert required_class("apps.event.authorizations.list") == "app-level"
assert required_class("apps.manifest.export") == "app-config"
assert required_class("conversations.history") == "bot-or-user"
def test_bot_token_refused_by_an_admin_method_is_the_finding():
state, detail = verdict("admin.teams.list", "bot",
{"ok": False, "error": "not_allowed_token_type"})
assert state == "wrong-class"
assert "org-admin-user" in detail
def test_an_argument_error_proves_the_class_was_accepted():
state, detail = verdict("apps.event.authorizations.list", "app-level",
{"ok": False, "error": "event_context_not_found"})
assert state == "class-accepted"
assert "after it has accepted the credential" in detail
def test_missing_scope_is_explicitly_not_this_notes_finding():
state, detail = verdict("conversations.history", "bot",
{"ok": False, "error": "missing_scope",
"needed": "channels:history"})
assert state == "scope-not-class"
assert "not a token-class one" in detail
def test_a_dead_credential_says_nothing_about_class():
state, _ = verdict("team.info", "bot", {"ok": False, "error": "token_revoked"})
assert state == "credential"
def test_success_is_reported_plainly():
assert verdict("team.info", "bot", {"ok": True})[0] == "allowed"
def test_an_unfamiliar_error_is_not_guessed_at():
assert verdict("team.info", "bot", {"ok": False, "error": "ratelimited"})[0] == "inconclusive"
def test_bot_id_in_auth_test_identifies_a_bot_token():
state, detail = token_class({"ok": True, "bot_id": "B1", "team_id": "T1"})
assert state == "bot"
assert "B1" in detail
def test_a_user_token_needs_users_info_to_be_called_an_admin():
plain = token_class({"ok": True, "user_id": "U1", "team_id": "T1"})
assert plain[0] == "user"
assert "admin status unknown" in plain[1]
admin = token_class({"ok": True, "user_id": "U1", "team_id": "T1"},
{"user": {"is_owner": True}})
assert admin[0] == "org-admin-user"
def test_a_non_admin_user_token_is_named_as_one_admin_will_refuse():
state, detail = token_class({"ok": True, "user_id": "U1"},
{"user": {"is_admin": False, "is_owner": False}})
assert state == "user"
assert "will refuse it" in detail
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { requiredClass, tokenClass, verdict } from './slack-method-token-class.mjs';
test('method families map to the class they want', () => {
assert.equal(requiredClass('admin.teams.list'), 'org-admin-user');
assert.equal(requiredClass('apps.connections.open'), 'app-level');
assert.equal(requiredClass('apps.event.authorizations.list'), 'app-level');
assert.equal(requiredClass('apps.manifest.export'), 'app-config');
assert.equal(requiredClass('conversations.history'), 'bot-or-user');
});
test('bot token refused by an admin method is the finding', () => {
const [state, detail] = verdict('admin.teams.list', 'bot',
{ ok: false, error: 'not_allowed_token_type' });
assert.equal(state, 'wrong-class');
assert.match(detail, /org-admin-user/);
});
test('an argument error proves the class was accepted', () => {
const [state, detail] = verdict('apps.event.authorizations.list', 'app-level',
{ ok: false, error: 'event_context_not_found' });
assert.equal(state, 'class-accepted');
assert.match(detail, /after it has accepted the credential/);
});
test('missing scope is explicitly not this notes finding', () => {
const [state, detail] = verdict('conversations.history', 'bot',
{ ok: false, error: 'missing_scope', needed: 'channels:history' });
assert.equal(state, 'scope-not-class');
assert.match(detail, /not a token-class one/);
});
test('a dead credential says nothing about class', () => {
const [state] = verdict('team.info', 'bot', { ok: false, error: 'token_revoked' });
assert.equal(state, 'credential');
});
test('success is reported plainly', () => {
assert.equal(verdict('team.info', 'bot', { ok: true })[0], 'allowed');
});
test('an unfamiliar error is not guessed at', () => {
assert.equal(verdict('team.info', 'bot', { ok: false, error: 'ratelimited' })[0],
'inconclusive');
});
test('bot_id in auth.test identifies a bot token', () => {
const [state, detail] = tokenClass({ ok: true, bot_id: 'B1', team_id: 'T1' });
assert.equal(state, 'bot');
assert.match(detail, /B1/);
});
test('a user token needs users.info to be called an admin', () => {
const plain = tokenClass({ ok: true, user_id: 'U1', team_id: 'T1' });
assert.equal(plain[0], 'user');
assert.match(plain[1], /admin status unknown/);
const admin = tokenClass({ ok: true, user_id: 'U1', team_id: 'T1' },
{ user: { is_owner: true } });
assert.equal(admin[0], 'org-admin-user');
});
test('a non admin user token is named as one admin will refuse', () => {
const [state, detail] = tokenClass({ ok: true, user_id: 'U1' },
{ user: { is_admin: false, is_owner: false } });
assert.equal(state, 'user');
assert.match(detail, /will refuse it/);
});
FAQ
How is this different from invalid_auth?
invalid_auth means the credential itself was not accepted anywhere: wrong class for the whole API, revoked, mangled or foreign. not_allowed_token_type means the credential is genuinely valid and this particular method will not take that class. The first is a problem with the token, the second is a problem with the routing, and they are fixed on different screens.
Why can a bot token never call admin methods?
Because Grid administration is modelled as authority a person holds. The admin.* family requires a user token belonging to an org owner or admin, with the matching admin.*:read scope on top of that. There is no bot scope that confers it, so the answer is never to add a scope to the bot; it is to use an admin's user token for those calls.
Is an argument error really good news?
For this audit, yes. A method validates the credential before it validates arguments, so an error naming a missing or invalid parameter is proof the class was accepted. The script reports it as class-accepted rather than as a failure, because a probe deliberately called with useless arguments is meant to get exactly that answer.
Why does the app-level token have to go in the header?
apps.connections.open and apps.event.authorizations.list accept the app-level token as Authorization: Bearer and reject it when it arrives as a form parameter, which surfaces as the same not_allowed_token_type. If a call fails with the right token in the right variable, check where in the request it is being placed before anything else.
Should I just hold every class of token so nothing is refused?
No. Each credential you hold is a credential that can leak, and an org admin's user token is the most dangerous one in the set. Hold only the classes the app actually needs, route each call to the right one explicitly, and if a single admin.* call is the only thing forcing you to keep a user token, ask whether that call is worth the blast radius.
Related field notes
- the token in the slot is the wrong class
- missing_scope names the scope you need
- scopes the app has never called
Sources
Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.
- Token types — Slack Docs
- apps.event.authorizations.list method reference — Slack Docs
- admin.teams.list method reference — Slack Docs
- users.info method reference — Slack Docs
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.