API

Публичный API — для своих скриптов и ботов: каналы, сообщения, реакции, файлы, задачи и люди. Всё, что здесь описано, останется в силе: в версии v1 API только дополняется.

Быстрый старт

  1. Выпустите токен: Настройки → Токены API → Новый токен. Выберите пространство, области доступа (что токену можно) и срок, подтвердите паролем. Токен показывается один раз — скопируйте его сразу.
  2. Проверьте его:
    curl -H "Authorization: Bearer ktx_p_…" https://app.kontext.su/api/v1/me
  3. Напишите в канал:
    curl -X POST https://app.kontext.su/api/v1/channels/42/messages \
      -H "Authorization: Bearer ktx_p_…" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: report-2026-09-19" \
      -d '{"text": "Отчёт дежурного: всё спокойно"}'
    Под сообщением в ленте появится неяркая подпись «через API · имя токена».

Токены

Токен — пропуск скрипта в API. Передавайте его только в заголовке Authorization: Bearer ktx_…. В адресе запроса (?token=…) токен не принимается нигде: адреса запросов попадают в журналы.

ВидПрефиксКто выпускаетСрок
Личныйktx_p_Сам человек, «Настройки → Токены API»Обязателен: 7, 30, 90 (по умолчанию) или 365 дней
Ботаktx_b_Тот, у кого есть право «Интеграции», в карточке ботаБез срока, 30, 90 или 365 дней
  • Одно пространство. Токен выпускается для одного пространства, и API берёт пространство из токена — в путях его нет. На запрос к объекту из другого пространства приходит 404, как будто объекта нет.
  • Прав не больше, чем у хозяина. Токен может только то, что одновременно разрешают его области доступа и роль его хозяина (человека или бота), и только в пространстве токена. Гость, например, через API видит ровно то же, что в приложении.
  • Выпуск подтверждается паролем, а если включена двухфакторная защита (2FA), — ещё и кодом из приложения. Тем, кто входит только через корпоративную учётную запись (SSO), пароль не нужен, но с момента входа должно пройти не больше 10 минут — иначе придётся войти заново. О выпуске личного токена приходит письмо (если почта не подтверждена — сообщение в личку от помощника), за 7 дней до конца срока — ещё одно. О скором конце срока токена бота пишут ответственному за бота.
  • Токен перестаёт работать сам, если хозяин вышел из пространства, стал гостем или ему закрыли доступ. Смена пароля и «Выйти везде» токены не отключают — чтобы скрипты не ломались от смены пароля; после неё проверьте список токенов в настройках.
  • Администратор пространства видит все действующие токены («Интеграции → Токены»): имя, хозяина, области доступа, последнее использование — и может отозвать любой. Сами значения токенов не видит никто. Там же он может выключить личные токены для всего пространства: они перестанут работать, но не будут отозваны — если включить их снова, скрипты заработают.
  • Гостю личные токены не выдаются.

Области доступа

ОбластьЧто открывает
—GET /v1/me — доступен любому токену
users:readЛюди пространства — те, кого хозяин токена видит
channels:readСписок каналов, канал, участники канала
channels:writeСоздать канал, сменить тему и название, вступить в публичный, выйти, добавить и убрать участника
messages:readИстория, тред, одно сообщение, поиск, свои эмодзи пространства
messages:writeПисать, править и удалять свои сообщения, реакции, личка, сообщения «видно только вам» (только бот)
files:readСкачать файл
files:writeЗагрузить файл, чтобы приложить его к сообщению или задаче
tasks:readЗадачи, комментарии, история задачи, проекты, метки
tasks:writeСоздать и изменить задачу, комментировать
usergroups:writeСоздать группу людей, изменить её, её состав и каналы

Не хватает области доступа — ответ 403 с кодом missing_scope, а в списке needed — каких областей не хватает.

Общие правила

  • Время — ISO 8601 в UTC: 2026-09-19T10:15:00Z.
  • Id — целые числа. Задачи — по номеру в пространстве (#123 в приложении → /v1/tasks/123).
  • Списки — {"items": […], "next_cursor": "…"}. limit от 1 до 200, по умолчанию 50. Следующая страница — тот же запрос с cursor=<next_cursor>; next_cursor: null — страниц больше нет. Курсор не разбирайте и не меняйте: передавайте его как есть.
  • Неизвестные поля и значения клиент должен игнорировать: в v1 появляются новые методы, поля и значения перечислений. Удаление и переименование — только в /v2; после выхода второй версии прежняя работает ещё не меньше полугода, с заголовками Deprecation и Sunset.
  • Idempotency-Key у POST /v1/channels/{id}/messages и POST /v1/tasks: повтор с тем же ключом в течение суток не создаёт второй объект, а возвращает первый (с заголовком Idempotent-Replayed: true). Пока первый запрос ещё выполняется, повтор получает 409 — повторите через пару секунд.
  • Ограничение частоты: 300 запросов в минуту на чтение и 60 на запись — на каждый токен, а не на IP-адрес: скрипты одного офиса за общим NAT не делят предел между собой. В каждом ответе есть X-RateLimit-Limit и X-RateLimit-Remaining; при превышении — 429 с Retry-After в секундах.

Ошибки

Тело у всех ошибок устроено одинаково: detail — по-русски, для человека; code — для программы.

{"detail": "Срок токена «Отчёт» истёк 2026-09-01 — выпустите новый",
 "code": "token_expired"}
HTTPКодКогда
401invalid_authНет заголовка, в нём не токен или такого токена нет
401token_revokedТокен отозван
401token_expiredСрок истёк
401token_inactiveХозяин больше не участник пространства, стал гостем, или в пространстве выключены личные токены
401user_inactiveДоступ хозяина закрыт
401bot_disabledБот выключен
403missing_scopeНе хватает области доступа; какой — в needed
403forbiddenРоль хозяина этого не позволяет
404not_foundОбъекта нет — или к нему нет доступа (ответ одинаковый нарочно)
409conflictЗапрос с этим Idempotency-Key ещё выполняется; у групп — @handle или название уже заняты (причина — в reason)
410goneОтветы через адрес команды (response_url) кончились или команда выключена — повторять бесполезно
413too_largeФайл больше предела
422invalid_requestЗапрос не прошёл проверку; fields — поле → что с ним не так
422too_many_reactionsНа сообщении уже 50 разных эмодзи: новое не поставить, но к уже поставленной реакции присоединиться можно
423workspace_read_onlyПространство только для чтения (подписка не оплачена): чтение работает, запись — нет; в теле также workspace_id и state
429rate_limitedСлишком много запросов; подождите Retry-After секунд
507no_spaceВ пространстве или на сервере кончилось место

Коды у 401 разные нарочно: ответ «срок истёк» вместо «неверный токен» сразу называет причину и экономит час поиска.

Токен API работает только с /api/v1 и /api/slack. На остальных адресах приложения — в том числе на веб-сокете и на MCP-коннекторе — запрос с ним получает 401 «Токен API работает только с /api/v1 и /api/slack».

Методы

Кто я

GET /v1/me — не требует области доступа.

{
  "kind": "person",
  "user": {"id": 7, "username": "anna", "display_name": "Анна",
           "is_bot": false, "deactivated": false, "avatar_url": null},
  "workspace": {"id": 3, "name": "Ромашка", "slug": "romashka"},
  "token": {"name": "Отчёт дежурного", "kind": "personal",
            "prefix": "ktx_p_Ab3dE", "scopes": ["messages:read"],
            "expires_at": "2026-12-18T10:00:00Z"},
  "bot_id": null
}

Люди — users:read

ЗапросЧто
GET /v1/usersЛюди пространства, которых видит хозяин токена; q — часть логина или имени, limit, cursor
GET /v1/users/{id}Один человек
GET /v1/users/by-username/{login}По логину (@ можно не писать)

User: id, username, display_name, is_bot, deactivated, avatar_url. Адресов почты API не отдаёт.

Каналы

Чтение — область channels:read, изменение — channels:write, личка — messages:write.

ЗапросЧто
GET /v1/channelsСписок; фильтры: types (channel, dm, voice), member, archived. member=true — каналы, где состоит хозяин токена; false — публичные, куда можно вступить
GET /v1/channels/{id}Канал
GET /v1/channels/{id}/membersУчастники, страницами
POST /v1/channelsПоля name, is_private, topic → 201; роль должна разрешать создавать каналы
PATCH /v1/channels/{id}Поля topic, name, archived — те же правила, что в приложении
POST /v1/channels/{id}/joinВступить можно только в публичный
POST /v1/channels/{id}/leaveВыйти → 204
POST /v1/channels/{id}/membersПоле user_id — добавить
DELETE /v1/channels/{id}/members/{user_id}Убрать → 204
POST /v1/dm{"user_ids": [5]} → личка; если такая уже есть, вернётся она

Channel: id, kind (channel, dm или voice), name (у лички null), is_private, topic, archived, is_member, created_at.

Сообщения

Чтение и поиск — область messages:read, остальное — messages:write.

ЗапросЧто
GET /v1/channels/{id}/messagesСообщения канала, кроме ответов в тредах, новые первыми; since, limit, cursor
GET /v1/messages/{id}Одно сообщение
GET /v1/messages/{id}/repliesОтветы в треде, старые первыми; id — первое сообщение треда или любой ответ в нём
POST /v1/channels/{id}/messagesПоля text, blocks, thread_id, file_ids, cite → 201; поддерживает Idempotency-Key
PATCH /v1/messages/{id}Поля text, blocks — только свои сообщения; blocks: null — не менять карточку, [] — убрать её
DELETE /v1/messages/{id}Своё — или любое, если роль разрешает удалять чужие → 204
PUT /v1/messages/{id}/reactions/{emoji}Поставить реакцию
DELETE /v1/messages/{id}/reactions/{emoji}Снять свою
POST /v1/messages/ephemeralТолько бот: поля channel_id, user_id, text, blocks, thread_id — сообщение «видно только вам»
GET /v1/search/messagesПоиск: q, channel_id, author_id, from, to, limit (до 100, по умолчанию 30; одна страница, без курсора). Находит ровно то, что хозяин токена видит в приложении
  • Реакцию ставят и снимают отдельными запросами, а не переключают: скрипты повторяют запросы, и переключатель при повторе то ставил бы реакцию, то снимал. Повторный PUT ничего не меняет. emoji — сам символ (🔥, в адресе — в URL-кодировке), имя Slack (thumbsup, +1, white_check_mark, eyes, rocket…; с тоном кожи — +1::skin-tone-3) или своё эмодзи пространства (partyparrot или :partyparrot:, псевдоним тоже подходит). Неизвестное эмодзи — 422 invalid_request. Разных эмодзи на сообщении не больше 50: новое сверх предела — 422 too_many_reactions, а к уже поставленной реакции присоединиться можно всегда.
  • thread_id — ответить в тред (id первого сообщения треда или любого ответа в нём).
  • file_ids — сначала загрузите файл (POST /v1/files), потом приложите его id.
  • text — с той же разметкой, что в поле ввода приложения: **жирный**, _курсив_, `код`, списки, цитаты >, [подпись](адрес), :эмодзи:, свои эмодзи пространства тоже. Внутри blocks — mrkdwn Slack.
  • Упоминание — @login в тексте, как в приложении; @группа уведомляет тех её участников, кто есть в канале; @channel и @everyone уведомляют весь канал, @here — тех, кто сейчас в сети.
  • Сообщение «видно только вам» не хранится: получатель видит его, пока не перезагрузит страницу. И бот, и получатель должны состоять в канале.
  • blocks — карточка в формате Slack Block Kit (массив блоков, как у Slack): секции с полями, картинки, контекст, кнопки, списки, даты. Кнопки и списки — только у токена бота: нажатия приходят на его адрес действий (см. «Кнопки и окна»); у личного токена они убираются, а кнопки-ссылки остаются. text рядом — запасной текст для push-уведомления. Ошибка в блоках — 422 invalid_request с fields.blocks (тексты ошибок как у Slack, с указанием места). Предупреждения (например, что неподдерживаемый блок показан пометкой) приходят в заголовке Kontext-Warning.

Message: id, channel_id, thread_id (id первого сообщения треда; null — если это не ответ в треде), author (User), text, created_at, edited_at, deleted, reply_count, reactions ([{emoji, name, count, user_ids, url}]: emoji — символ или :имя: своего эмодзи, name — имя Slack без двоеточий, url — картинка своего эмодзи, у стандартного — null), files ([{id, name, mime, size}]), via ({"kind": "api", "name": "Отчёт дежурного"} у сообщения, написанного личным токеном, иначе null), permalink, blocks (карточка в том виде, в каком её прислали; null — обычное сообщение), legacy_attachments (вложения Slack, если сообщение отправлено через Slack API).

Файлы

ЗапросЧто
POST /v1/files?name=отчёт.pdfОбласть files:write. Тело запроса — сам файл, тип — из Content-Type → 201 {id, name, mime, size}
GET /v1/files/{id}Область files:read. Скачать файл — только если хозяин токена видит его в этом пространстве
curl -X POST "https://app.kontext.su/api/v1/files?name=report.pdf" \
  -H "Authorization: Bearer ktx_p_…" -H "Content-Type: application/pdf" \
  --data-binary @report.pdf

Предел размера — как в приложении (файл больше — 413, too_large). Если в пространстве кончилось место, ответ 507 (no_space) приходит сразу, ещё до конца загрузки.

Свои эмодзи — messages:read

ЗапросЧто
GET /v1/emojiСвои эмодзи пространства по алфавиту, вместе с псевдонимами; limit до 1000 (по умолчанию 200), cursor
GET /v1/emoji/files/{key}Картинка эмодзи (адрес — поле url из списка), токен — в заголовке; картинка из другого пространства — 404

Emoji: name, url, alias_of (у псевдонима — имя исходного эмодзи, картинка тоже от него), animated, created_at. В тексте и реакциях своё эмодзи пишется как :name:. Добавлять и удалять эмодзи через API нельзя, только в приложении.

Задачи

Чтение — область tasks:read, изменение и комментарии — tasks:write.

ЗапросЧто
GET /v1/tasksstatus (через запятую), assignee (id или me), project_id, label_id, priority, q, include_closed; без status — только открытые; новые номера первыми
GET /v1/tasks/{number}Задача
POST /v1/tasksПоля title, description, status, priority, due_at, assignee_id, project_id, label_ids, is_private, viewer_ids, file_ids → 201; поддерживает Idempotency-Key
PATCH /v1/tasks/{number}Те же поля; чтобы очистить поле, перечислите его в clear — можно due_at, assignee_id, project_id
GET /v1/tasks/{number}/commentsКомментарии, старые первыми
POST /v1/tasks/{number}/commentsПоля text, file_ids → 201
GET /v1/tasks/{number}/historyКто и что менял — например, кто перенёс срок
GET /v1/projectsПроекты; archived=false — без архивных
GET /v1/labelsМетки

Статусы: todo, in_progress, testing, done, cancelled. Приоритеты: low, normal, high, urgent. Удалять задачи через API нельзя — только в приложении.

# мои открытые задачи
curl -H "Authorization: Bearer ktx_p_…" \
  "https://app.kontext.su/api/v1/tasks?assignee=me"

Task: number, title, description, status, priority, due_at, assignee, project, labels, is_private, created_by, created_at, completed_at, url.

Группы людей

Группа — имя вида @handle, которым в тексте упоминают сразу всех её участников, кто есть в канале (@devs, @oncall).

ЗапросОбластьЧто
GET /v1/groupsusers:readГруппы пространства; ?disabled=true — вместе с отключёнными; limit, cursor
GET /v1/groups/{id}users:readГруппа
POST /v1/groupsusergroups:write{"handle", "name", "description", "members", "channels"} → 201; каналы — только публичные
PATCH /v1/groups/{id}usergroups:writehandle, name, description; "disabled": true — отключить (упоминание группы никого не уведомляет, но @handle остаётся за ней)
PUT /v1/groups/{id}/membersusergroups:write{"members": [12, 15]} — новый состав целиком: так бот дежурств меняет, кто сейчас в @oncall

Group: id, handle, name, description, members, channels, disabled, created_at, updated_at. Создавать группы может хозяин токена с правом «Заводить группы людей» (у гостевой роли этого права не бывает), менять — автор группы, её участник или тот, у кого есть право «Управлять группами». Если у хозяина токена гостевая роль, групп ему не видно: список пустой, запрос одной группы — 404. При отказе в ответе есть поле reason (handle_taken, is_login, invalid_members, too_many_members…) и, где нужно, ids — какие id не подошли.

Боты

Бот — учётная запись пространства для интеграций: пишет от своего имени (с пометкой «бот»), работает с API по своему токену и не занимает места в подписке. Создаёт его тот, у кого есть право «Интеграции»: Пространство → Интеграции → Боты → Новый бот.

  • Что бот видит — решает его роль. Рекомендуем «только каналы, куда добавите» (гостевая роль): бот видит только те каналы, куда его добавили. Роль «как участник» открывает публичные каналы и задачи — она нужна, например, боту, который синхронизирует задачи.
  • У бота не больше прав, чем у того, кто его настраивает. Если у вас нет права управлять пространством, боту можно дать только роль, все права которой есть у вас самих. Если роль бота шире ваших прав, выпустить ему токен и сменить секрет может администратор пространства: токен бота действует с правами бота.
  • В канал бота добавляют в его карточке («Каналы»). У кого есть токен бота, тот может читать всё, что видит бот, — поэтому перед добавлением бота в приватный канал приложение показывает, у кого его токены.
  • Секрет подписи — им Kontext подписывает свои запросы к серверу бота (события, слеш-команды и нажатия кнопок) по схеме Slack v0. Секрет показывается один раз, сменить его можно в карточке.
  • Выключить — токены бота перестают работать (bot_disabled), события не приходят, команды не срабатывают; бота можно включить обратно. Удалить — учётная запись бота закрывается, токены отзываются; сообщения бота остаются.
  • Входящий вебхук от имени бота: в настройках вебхука («Интеграции → Вебхуки») выберите «Писать от имени: бота». Бот сам добавится в канал, и вебхук больше не будет зависеть от того, в каких каналах сейчас состоит человек. Подробнее — Входящие вебхуки.

Исходящие события

Бот может получать события пространства на свой адрес: карточка бота → «События» → Новая подписка. В подписке задают адрес, формат (Kontext, Slack или Mattermost), события, каналы (все каналы бота или выбранные) и галочку «и сообщения ботов». У бота может быть до 10 подписок, в пространстве — до 50.

СобытиеКогдаВ data
message.createdСообщение или ответ в тредеmessage, channel
message.mentionТо же, но только сообщения, где бота упомянули @логиномmessage, channel
message.updatedАвтор изменил сообщениеmessage, channel
message.deletedСообщение удаленоmessage_id, channel
reaction.added, reaction.removedПоставлена или снята реакцияmessage_id, emoji (в том виде, в каком хранится), name (имя Slack без двоеточий), user, channel
member.joined, member.leftЧеловек вступил в пространство, вышел из него или его убралиuser
channel.createdСоздан публичный каналchannel
task.created, task.updated, task.deletedЗадача создана, изменена, удаленаtask, changes (при изменении — [{field, old, new}])
task.commentedКомментарий к задачеtask, comment

Если бот отвечает только на обращения к нему, подпишите его на message.mention: message.created присылает каждое сообщение канала. Если подписка включает оба события, сообщение с упоминанием придёт дважды — у доставки упоминания event_id оканчивается на _m.

Кто что получает. События канала приходят, если бот состоит в канале (или это личка с ботом) и канал проходит фильтр подписки. О собственных действиях бот событий не получает; сообщения других ботов и вебхуков — только с галочкой «и сообщения ботов». Тот, кто создал подписку, тоже должен видеть канал: иначе сообщения приватного канала уходили бы на адрес человека, которого туда не приглашали. Если он ушёл из канала или из пространства, события оттуда больше не приходят, а в карточке подписки появляется предложение переназначить её на себя. События о вступлении, выходе и новом канале получают боты с любой ролью, кроме гостевой. События задач — если задачу видят и бот, и автор подписки.

Тело (формат Kontext) — в data те же объекты, что возвращают методы выше:

{
  "type": "event_callback",
  "event_id": "evt_3f2c9a…",
  "event": "message.created",
  "event_time": "2026-09-19T10:15:00Z",
  "workspace": {"id": 3, "slug": "romashka"},
  "bot": {"id": 12, "user_id": 45},
  "data": {
    "message": {"id": 901, "text": "Сборка упала", "…": "…"},
    "channel": {"id": 42, "…": "…"}
  }
}

Заголовки: X-Kontext-Event, X-Kontext-Delivery (он же event_id), X-Kontext-Request-Timestamp и X-Kontext-Signature (те же значения повторяются в X-Slack-Request-Timestamp и X-Slack-Signature), при повторной доставке — X-Kontext-Retry-Num; User-Agent: Kontext-Hooks/1.0.

Подпись — по схеме Slack v0, секретом подписи бота: v0= + hex(HMAC-SHA256(секрет, v0:<отметка>:<тело>)), где отметка — время запроса из заголовка. Запросы с отметкой старше пяти минут отклоняйте.

import hmac, time

def signed(secret: str, headers, body: bytes) -> bool:
    ts = headers["X-Kontext-Request-Timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    base = b"v0:" + ts.encode() + b":" + body
    mine = "v0=" + hmac.new(secret.encode(), base, "sha256").hexdigest()
    return hmac.compare_digest(mine, headers["X-Kontext-Signature"])

Ответ и повторы. Любой ответ 2xx в течение 5 секунд — событие доставлено; что в теле ответа, не важно (кроме формата Mattermost), его начало видно в журнале. Иначе Kontext повторяет доставку через 1 минуту, 5 минут, 30 минут, 2 часа и 6 часов, а затем помечает её «не доставлено». Перенаправление (3xx) считается неудачей: укажите конечный адрес. Ответ 410 Gone сразу выключает подписку, 20 окончательных неудач подряд — тоже; ответственному за бота приходит сообщение в личку, включить подписку снова можно в карточке. Доставка «хотя бы раз»: одно событие может прийти дважды — отбрасывайте повторы по event_id; порядок событий не гарантирован.

  • Проверка связи — при сохранении и по кнопке «Проверить»: приходит событие ping с пустым data.
  • Журнал доставок — в карточке подписки, за 7 дней: код ответа, время, начало ответа; кнопка «Повторить» — для одной доставки или для всех неудачных за сутки.
  • Адрес — только http или https и только в интернете: адреса внутренних сетей (10.…, 192.168.…, 127.…, 169.254.… и подобные) отклоняются и при сохранении, и при каждой доставке; по перенаправлениям Kontext не переходит.
  • Зацикливание. Если два бота в одном канале отвечают друг другу по кругу, подписка выключается. Если на подписку приходится больше 600 событий в минуту, лишние за эту минуту отбрасываются; пять таких минут подряд — и подписка выключается.
  • Неоплаченное пространство (только для чтения) событий не отправляет; те, что ждали доставки, отбрасываются.

Формат Slack — Events API (event_callback с token, team_id, api_app_id, authorizations): message.created — message, message.mention — app_mention, правка и удаление — message с subtype message_changed и message_deleted, реакции — reaction_added и reaction_removed, вступление — team_join, канал — channel_created; событий задач и выхода у Slack нет. Проверка связи — url_verification с challenge (Bolt отвечает на неё сам). Подробнее — Боты со Slack.

Формат Mattermost — поля исходящего вебхука Mattermost; приходят только новые сообщения со словом-триггером или из выбранных каналов, а ответ скрипта бот публикует от своего имени — Боты с Mattermost.

Слеш-команды

Если начать сообщение с /, появится список команд. Встроенные: /remind <когда> <что> (напоминание себе в личку: «через 20 минут», «завтра в 10», «в пятницу в 15:00»), /task <название> [@кто] [до <когда>], /topic, /invite @кто, а в самом приложении ещё /help, /me, /shrug, /away, /dnd, /online, /status, /mute, /unmute, /search, /summary, /ask, /call, /msg @кто, /join #канал, /leave. Эти имена нельзя дать своей команде.

Своя команда создаётся в карточке бота → «Команды»: имя (латиница, кириллица, цифры, _ и -, до 32 знаков), адрес, метод (POST или GET), описание и подсказка к аргументам. Проверочный токен показывается один раз. В пространстве — до 50 своих команд; гости их не видят.

Запрос приходит формой (POST) или в строке запроса (GET) — сразу с полями и Slack, и Mattermost: token, team_id, team_domain, enterprise_id, enterprise_name, is_enterprise_install, api_app_id, channel_id, channel_name, user_id, user_name, command, text, response_url, trigger_id, user_mentions, user_mentions_ids, channel_mentions, channel_mentions_ids, root_id и thread_ts (команда в треде) — и kontext_workspace_id, kontext_channel_id, kontext_user_id для скриптов, которые потом обращаются к API. Заголовки: подпись v0 секретом бота (как у событий), Authorization: Token <проверочный токен>, User-Agent: Kontext-Commands/1.0.

Ответ. Если сервер бота не ответил за 3 секунды, вызвавший видит «ждём ответ…»; ответ принимается в течение 30 секунд. Команда не повторяется никогда: деплой не запустится дважды из-за таймаута.

  • пустой 200 — ничего не показывать;
  • простой текст — сообщение «видно только вам» для вызвавшего;
  • JSON {"response_type": "ephemeral" | "in_channel", "text", "attachments", "blocks"}: при in_channel бот пишет ответ в канал (даже если сам в нём не состоит), а над ответом видно, кто и какую команду вызвал; если команду вызвали в треде, ответ придёт в тред; блоки и вложения показываются карточкой, и её кнопки работают;
  • поддерживаются также replace_original, delete_original, extra_responses (до пяти), channel_id (канал, где состоит бот и который видит вызвавший); username и icon_* не действуют.

response_url — чтобы ответить позже или несколько раз: отправьте POST на адрес из поля response_url (https://app.kontext.su/api/v1/respond/<секрет>) с тем же телом, что у обычного ответа. Токен не нужен — секрет уже есть в адресе; адрес действует 30 минут и принимает до пяти ответов. Ошибки — в общем формате API: 404 — адрес устарел или не существует, 410 (gone) — ответы кончились или команда выключена, 413 — ответ больше 64 КБ, 423 — пространство только для чтения, 429 — запросы чаще 30 раз в минуту.

Кнопки и окна

Кнопки, списки и модальные окна приложений — в формате Slack: приложение на Bolt работает без правок.

  • Адрес действий — один на бота, как Interactivity Request URL у Slack: карточка бота → «Интерактивность». Для Bolt это тот же /slack/events, что у событий. Задаёт его тот, кто может выпустить боту токен.
  • Нажатие приходит запросом POST с формой payload=<JSON> — тело block_actions, как у Slack (user, trigger_id, container, channel, message, state, response_url, actions), с подписью v0 секретом бота и User-Agent: Kontext-Interactive/1.0. Ответить нужно за 3 секунды; нажатие не повторяется никогда — «Одобрить» не отправится дважды. Нажать кнопку может любой, кто видит сообщение; кому можно «Одобрить», решает приложение по user.id. Kontext гарантирует, что этот id настоящий, а кнопка действительно из этого сообщения.
  • response_url нажатия — как у команды: по умолчанию — новое сообщение «видно только вам» для нажавшего; replace_original заменяет исходную карточку (текст и блоки целиком), delete_original — удаляет её, in_channel — пишет в канал. Адрес действует 30 минут, до пяти ответов.
  • Окна — views.open, views.update, views.push по trigger_id из нажатия или команды: он действует 10 секунд и только один раз. Окно видит тот, кто нажал, — в той вкладке, где нажал. Поверх друг друга — до трёх окон. На view_submission отвечайте response_action: errors (ошибки под полями), update, push, clear; пустой 200 закрывает окно. view_closed приходит, если у окна задан notify_on_close. Через час после последнего действия окно устаревает.
  • Кнопка со ссылкой (url) открывает ссылку, а нажатие приложению не отправляет.
  • Не кладите секреты в value и private_metadata: value кнопки доходит до устройства каждого, кто читает канал; private_metadata окна на устройства людей не передаётся, но это поле для данных вашего приложения, а не хранилище секретов.
  • Картинки из блоков Kontext копирует к себе: у читателей канала они загружаются не с сервера вашего приложения.
  • Ярлыки — как Shortcuts у Slack: карточка бота → «Интерактивность» → «Ярлыки» (вид, название, описание, callback_id). Глобальный ярлык (shortcut) виден в подсказке, которая появляется после «/» в поле ввода; в теле нет канала и response_url, и приложение открывает окно по trigger_id. Ярлык сообщения (message_action) — значок молнии в панели сообщения; он есть только в каналах, которые читает сам бот. В теле — channel, message и response_url. Над ответом в канал видно, кто и каким ярлыком его вызвал; чужое сообщение через этот адрес не изменить и не удалить.
  • external_select и multi_external_select — список, варианты которого приложение подбирает по набранному тексту: запрос block_suggestion уходит на тот же адрес действий, когда набрано min_query_length знаков (по умолчанию 3); ответить нужно {"options"} или {"option_groups"} за 3 секунды. Выбор сверяется с тем, что приложение предложило этому человеку, — подменить вариант или его текст нельзя.
  • Не поддерживаются: домашняя вкладка (views.publish), устаревшие интерактивные вложения и dialog.*, rich_text_input и file_input, разворачивание ссылок (unfurl). Неподдерживаемый блок в сообщении показывается пометкой, а в окне даёт ошибку invalid_arguments с указанием места.

Совместимость со Slack

Часть методов Slack Web API доступна по адресу https://app.kontext.su/api/slack/ — чтобы приложение на Bolt или slack_sdk заработало без переписывания:

from slack_sdk import WebClient

client = WebClient(token="ktx_b_…",
                   base_url="https://app.kontext.su/api/slack/")
client.chat_postMessage(channel="#продажи", text="Сборка прошла :rocket:")

Для @slack/web-api (Node) — new WebClient(token, { slackApiUrl: "https://app.kontext.su/api/slack/" }).

МетодОбластьЗаметки
auth.test—team_id, user_id, bot_id
chat.postMessagemessages:writechannel — C…, #имя, U… или @логин (личка); text, blocks, attachments (карточкой), thread_ts
chat.update, chat.deletemessages:writeТолько свои сообщения; blocks и attachments — по правилам Slack (text без blocks убирает блоки)
chat.postEphemeralmessages:writeТолько бот; user должен состоять в канале; blocks — можно с кнопками
chat.getPermalinkmessages:read
reactions.add, reactions.removemessages:writeИмя Slack (с тоном кожи — +1::skin-tone-2), своё эмодзи пространства или сам символ; если на сообщении уже 50 разных эмодзи, новое — ошибка too_many_emoji
reactions.getmessages:readchannel, timestamp → сообщение с reactions: [{name, users, count}]
emoji.listmessages:readСвои эмодзи пространства: имя → адрес картинки (открывается с токеном в заголовке) или alias:…
conversations.listchannels:readtypes, exclude_archived, limit, cursor
conversations.info, conversations.memberschannels:read
conversations.history, conversations.repliesmessages:readlatest, oldest, inclusive, limit, cursor
conversations.joinchannels:writeПубличный канал
conversations.openmessages:writeusers=U1,U2 → личка
users.list, users.infousers:readБез адресов почты
views.open, views.update, views.pushmessages:writeТолько токен бота; см. «Кнопки и окна»
usergroups.list, usergroups.users.listusers:readinclude_users, include_count, include_disabled; гостю — пустой список
usergroups.create, usergroups.update, usergroups.disable, usergroups.enable, usergroups.users.updateusergroups:writeБоту нужна роль «как участник»; users.update задаёт состав целиком

Не поддерживаются: users.lookupByEmail (адресов почты в API нет), files.* (файлы загружайте через POST /v1/files), views.publish (домашней вкладки нет), dialog.* (вместо него — views.open), chat.unfurl, reactions.list (реакции одного сообщения — через reactions.get). Они отвечают not_supported. Неизвестный метод — unknown_method.

Отличия, о которых стоит знать:

  • Id: канал C<число>, личка D<число>, человек U<число>, бот B<число>, пространство T<число>. Просто числа тоже принимаются.
  • ts сообщения — это номер, а не время: "12345.000000". Треды и правка по ts работают, но вычислять по ts возраст сообщения нельзя — берите время из самого сообщения.
  • Разметка: <@U12> в тексте становится настоящим упоминанием, если хозяин токена этого человека видит (иначе остаётся подпись из <@U12|подпись> или просто U12); ссылки <url|текст> становятся ссылками, известные короткие имена эмодзи (:rocket:, :white_check_mark:) — символами. username, icon_*, unfurl_*, reply_broadcast молча игнорируются.
  • Ответ при ошибке — всё равно 200 с {"ok": false, "error": "…"}, как у Slack, а рядом — detail по-русски. Исключения: 429 с Retry-After (SDK повторит сам) и 423 (restricted_action) — пространство только для чтения.
  • Коды ошибок — как у Slack: not_authed, invalid_auth, token_revoked, token_expired, account_inactive, missing_scope (с needed и provided), channel_not_found, not_in_channel, is_archived, message_not_found, cant_update_message, cant_delete_message, invalid_name, already_reacted, no_reaction, ratelimited.

Безопасность

  • Храните токен как пароль: в переменной окружения или хранилище секретов, не в коде и не в адресе запроса.
  • Если токен утёк, отзовите его («Настройки → Токены API» или администратор в «Интеграции → Токены»): он перестанет работать со следующего же запроса. Всё, что написано этим токеном, отмечено «через API · имя токена».
  • В каждой строке списка токенов видно, когда и с какого IP-адреса им пользовались последний раз и сколько запросов было за неделю: перед отзывом понятно, работает ли ещё скрипт.