[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-bots\u002Fevents":3,"docs-tabs-bots\u002Fevents":6},{"content":4,"lastmod":5},"# События\n\nПолучайте входящие события бота методом опроса и обрабатывайте их: новые сообщения, команды, реакции и добавление бота в чат.\n\n**Скоуп:** `imbot` | **Базовый URL:** `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1` | **Авторизация:** `X-Api-Key`\n\n## Операции\n\n- [Получить события опросом](.\u002Fevents\u002Fpolling.md) — `GET \u002Fv1\u002Fbots\u002F:botId\u002Fevents`\n- [Bot-события (ONIMBOTV2*)](.\u002Fevents\u002Fbot-events.md) — `GET ONIMBOTV2*`\n- [User-события (ONIMV2*)](.\u002Fevents\u002Fuser-events.md) — `GET ONIMV2*`\n","2026-06-16",{"events\u002Fpolling.md":7,"events\u002Fbot-events.md":8,"events\u002Fuser-events.md":9},"\n## Получить события (polling)\n\n`GET \u002Fv1\u002Fbots\u002F:botId\u002Fevents`\n\nОсновной механизм получения входящих сообщений и команд. Бот периодически запрашивает новые события.\n\nВайбкод хранит `lastOffset` в базе данных — при первом запросе без `offset` используется сохранённое значение. Это позволяет боту продолжить с места остановки после перезапуска.\n\n## Параметры\n\n| Параметр | Тип | Обяз. | По умолч. | Описание |\n|----------|-----|:-----:|-----------|---------|\n| `botId` (path) | number | да | — | ID бота |\n| `offset` (query) | number | нет | из БД | Начальная позиция. Без параметра — сохранённое в БД значение. `offset=0` — начать с начала |\n| `limit` (query) | number | нет | `100` | Максимальное количество событий (1-1000) |\n| `withUserEvents` (query) | boolean | нет | `false` | Включить [user-события](\u002Fdocs\u002Fbots\u002Fevents\u002Fuser-events) (ONIMV2*). Требует предварительной подписки — порядок настройки описан на странице User-события. Без подписки запрос вернёт `502 BITRIX_ERROR: User is not subscribed` |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fbots\u002F42\u002Fevents?limit=50\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\"\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fbots\u002F42\u002Fevents?limit=50\" \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\"\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch('https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fbots\u002F42\u002Fevents?limit=50', {\n  headers: {\n    'X-Api-Key': 'YOUR_API_KEY',\n  },\n})\n\nconst { success, data } = await res.json()\nconsole.log('События:', data.events.length, 'Ещё:', data.hasMore)\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nconst res = await fetch('https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fbots\u002F42\u002Fevents?limit=50', {\n  headers: {\n    'X-Api-Key': 'YOUR_APP_KEY',\n    'Authorization': 'Bearer USER_SESSION_TOKEN',\n  },\n})\n\nconst { success, data } = await res.json()\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|---------|\n| `events` | array | Массив событий (см. [типы событий](\u002Fdocs\u002Fbots\u002Fevents\u002Fbot-events)) |\n| `events[].eventId` | number | ID события — передайте как `offset` в следующем запросе |\n| `events[].type` | string | Код события (`ONIMBOTV2MESSAGEADD`, `ONIMBOTV2COMMANDADD` и т.д.) |\n| `events[].date` | string | Дата и время события (ISO 8601) |\n| `events[].data` | object | Данные события (структура зависит от типа, ключи в camelCase) |\n| `nextOffset` | number | Смещение для следующего запроса |\n| `hasMore` | boolean | Есть ещё необработанные события |\n| `storedOffset` | number | Текущее сохранённое смещение в БД |\n| `persisted` | boolean | `true` если `lastOffset` в БД продвинулся (доставлено хотя бы одно событие). `false` — ответ пустой, курсор не изменился |\n| `hint` | string | Диагностическое сообщение при устойчиво пустой очереди — указывает на состояние установки на стороне Битрикс24. Счётчик пустых ответов обновляется периодически, а не на каждый запрос, поэтому подсказка появляется после нескольких периодов пустоты, а не строго на пятом запросе (при рекомендованном интервале опроса 2–5 секунд — спустя примерно пару минут непрерывно пустого опроса). Число N в тексте — количество зафиксированных периодов пустоты, а не точное число сделанных запросов; для веток кода-логики опирайтесь на `persisted` и наличие событий, а `hint` трактуйте как подсказку |\n\n## Пример ответа\n\nЕсть новые события (`persisted: true`):\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"events\": [\n      {\n        \"eventId\": 35,\n        \"type\": \"ONIMBOTV2MESSAGEADD\",\n        \"date\": \"2026-04-13T17:15:00+03:00\",\n        \"data\": {\n          \"dialogId\": \"chat123\",\n          \"message\": { \"id\": 1501, \"text\": \"Привет, бот!\" },\n          \"user\": { \"id\": 1, \"name\": \"Иван Петров\" }\n        }\n      }\n    ],\n    \"nextOffset\": 36,\n    \"hasMore\": false,\n    \"storedOffset\": 35,\n    \"persisted\": true\n  }\n}\n```\n\nУстойчиво пустая очередь событий (появляется поле `hint`; число в тексте — количество зафиксированных периодов пустоты, а не сделанных запросов):\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"events\": [],\n    \"nextOffset\": 36,\n    \"hasMore\": false,\n    \"storedOffset\": 36,\n    \"persisted\": false,\n    \"hint\": \"Events queue has stayed empty across 7 consecutive checks (the empty-poll counter is sampled periodically, not once per request, so this reflects sustained emptiness rather than the exact number of polls). Bot config: eventMode='fetch', code='support_bot'. If the bot is alive on B24 (chats receive messages, im.bot.list lists it) the most common cause is B24-side event-subscription decay — run POST \u002Fv1\u002Fbots\u002F42\u002Fresubscribe first (lightweight, preserves openline \u002F WELCOME_BOT bindings). If that does not help, verify: (1) eventMode is 'fetch' (current: 'fetch'); (2) your OAuth app's INSTALL event handler responded 200 to Bitrix24; (3) the bot was added to a chat where messages are being sent (it must be a participant for chat-message events); (4) only if all of the above are confirmed — try POST \u002Fv1\u002Fbots to re-register (destroys openline bindings; last resort).\"\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n404 — бот не найден:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"BOT_NOT_FOUND\",\n    \"message\": \"Bot 999 not found. Register it first via POST \u002Fv1\u002Fbots.\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|---------|\n| 400 | `INVALID_BOT_ID` | `botId` не является числом |\n| 404 | `BOT_NOT_FOUND` | Бот с таким ID не найден |\n| 403 | `BOT_ACCESS_DENIED` | Бот принадлежит другому API-ключу |\n| 502 | `BITRIX_ERROR` | Ошибка Битрикс24 (текст ошибки в `message`) |\n| 403 | `SCOPE_DENIED` | API-ключ не имеет скоупа `imbot` |\n| 401 | `TOKEN_MISSING` | API-ключ не имеет настроенных токенов |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n**Какой токен использовать.** Для личного ключа `vibe_api_…` достаточно заголовка `X-Api-Key`. Для ключа авторизации `vibe_app_…` обязательно добавлять `Authorization: Bearer \u003Csession_token>` — без Bearer запрос вернёт `401 TOKEN_MISSING`. Получение `session_token` для OAuth-приложения — см. [Ключи и авторизация](\u002Fdocs\u002Fkeys-auth).\n\n**Серверное хранение offset:** Вайбкод хранит `lastOffset` в базе. При первом запросе без `offset` — используется сохранённое значение. После получения событий `lastOffset` обновляется автоматически, не дожидаясь результата записи.\n\n**offset=0:** явная передача `offset=0` начинает с начала истории — для отладки или первичной загрузки.\n\n**offset=N (конкретное значение):** само событие с указанным ID попадает в ответ. Чтобы не получить дубли, всегда передавайте `nextOffset` из предыдущего ответа, а не `eventId` последнего обработанного события.\n\n**Цепочка запросов:**\n\n```\nGET \u002Fevents           → { nextOffset: 42, hasMore: true }\nGET \u002Fevents?offset=42 → { nextOffset: 55, hasMore: false }\nGET \u002Fevents?offset=55 → { events: [], hasMore: false }\n```\n\n**Рекомендуемый интервал polling:** 2-5 секунд между запросами.\n\n**Polling-цикл (готовый пример):**\n\n```javascript\nconst BOT_ID = 42\nconst API_KEY = 'YOUR_API_KEY'\nconst BASE = 'https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1'\n\nasync function pollEvents() {\n  let offset = undefined\n\n  while (true) {\n    try {\n      const url = new URL(`${BASE}\u002Fbots\u002F${BOT_ID}\u002Fevents`)\n      if (offset !== undefined) url.searchParams.set('offset', String(offset))\n\n      const res = await fetch(url, {\n        headers: { 'X-Api-Key': API_KEY },\n      })\n      const { data } = await res.json()\n\n      for (const event of data.events ?? []) {\n        await handleEvent(event)\n      }\n\n      if (data.nextOffset !== undefined) {\n        offset = data.nextOffset\n      }\n    } catch (err) {\n      console.error('Poll error:', err.message)\n    }\n\n    await new Promise(r => setTimeout(r, 3000))\n  }\n}\n\nasync function handleEvent(event) {\n  const { data } = event\n\n  switch (event.type) {\n    case 'ONIMBOTV2MESSAGEADD':\n      \u002F\u002F Ответить на сообщение\n      await fetch(`${BASE}\u002Fbots\u002F${BOT_ID}\u002Fmessages`, {\n        method: 'POST',\n        headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application\u002Fjson' },\n        body: JSON.stringify({\n          dialogId: data.chat.dialogId,\n          fields: { message: `Получил: ${data.message.text}` },\n        }),\n      })\n      break\n\n    case 'ONIMBOTV2COMMANDADD':\n      \u002F\u002F Ответить на команду\n      await fetch(`${BASE}\u002Fbots\u002F${BOT_ID}\u002Fcommands\u002F${data.command.id}\u002Fanswer`, {\n        method: 'POST',\n        headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application\u002Fjson' },\n        body: JSON.stringify({\n          dialogId: data.chat.dialogId,\n          messageId: data.message.id,\n          fields: { message: `Команда \u002F${data.command.command}: ${data.command.params}` },\n        }),\n      })\n      break\n  }\n}\n\npollEvents()\n```\n\n## Смотрите также\n\n- [Диагностика проблем](\u002Fdocs\u002Fbots\u002Ftroubleshooting)\n- [Bot-события](\u002Fdocs\u002Fbots\u002Fevents\u002Fbot-events)\n- [User-события](\u002Fdocs\u002Fbots\u002Fevents\u002Fuser-events)\n- [Отправить сообщение](\u002Fdocs\u002Fbots\u002Fmessages\u002Fsend)\n- [Ответить на команду](\u002Fdocs\u002Fbots\u002Fcommands\u002Fanswer)\n- [Бот-платформа](\u002Fdocs\u002Fbots)\n- [Лимиты и оптимизация](\u002Fdocs\u002Foptimization)\n","\n## Bot-события (ONIMBOTV2*)\n\nПриходят автоматически для всех ботов с `eventMode: \"fetch\"`. Получаются через [polling](\u002Fdocs\u002Fbots\u002Fevents\u002Fpolling).\n\nКаждое событие имеет структуру:\n\n```json\n{\n  \"eventId\": 29,\n  \"type\": \"ONIMBOTV2JOINCHAT\",\n  \"date\": \"2026-04-13T17:12:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"chat3553\",\n    \"bot\": { ... },\n    \"chat\": { ... },\n    \"user\": { ... },\n    \"language\": \"ru\"\n  }\n}\n```\n\n| Поле | Тип | Описание |\n|------|-----|---------|\n| `eventId` | number | ID события — передайте как `offset` в следующем запросе polling |\n| `type` | string | Код типа события |\n| `date` | string | Дата и время (ISO 8601) |\n| `data` | object | Данные события (структура зависит от типа) |\n\n## Список событий\n\n| Тип | Описание |\n|-----|---------|\n| [ONIMBOTV2MESSAGEADD](#onimbotv2messageadd) | Новое сообщение боту |\n| [ONIMBOTV2MESSAGEUPDATE](#onimbotv2messageupdate) | Сообщение отредактировано |\n| [ONIMBOTV2MESSAGEDELETE](#onimbotv2messagedelete) | Сообщение удалено |\n| [ONIMBOTV2JOINCHAT](#onimbotv2joinchat) | Бот добавлен в чат |\n| [ONIMBOTV2COMMANDADD](#onimbotv2commandadd) | Вызвана slash-команда |\n| [ONIMBOTV2REACTIONCHANGE](#onimbotv2reactionchange) | Реакция на сообщение бота |\n| [ONIMBOTV2DELETE](#onimbotv2delete) | Бот удалён с портала |\n| [ONIMBOTV2CONTEXTGET](#onimbotv2contextget) | Запрошен контекст диалога |\n\n---\n\n## Как обрабатывать события\n\nЦикл опроса описан на странице [Получить события](\u002Fdocs\u002Fbots\u002Fevents\u002Fpolling). Получив массив `events`, маршрутизируйте каждое событие по полю `type`. Отвечать боту нужно в диалог `event.data.dialogId` (для групп — `chatXXX`, для личных — id пользователя, тот же, что `event.data.chat.dialogId`).\n\n**Перед обработкой сообщения отфильтруйте лишнее:**\n\n- `message.isSystem === true` — служебное сообщение (вход в чат, смена настроек). Пропускайте.\n- `message.authorId === event.data.bot.id` — собственное сообщение бота. Боты типа `personal` и `supervisor` получают все сообщения чата, включая свои ответы, — без этой проверки обработчик зациклится. Бот типа `bot` получает только личные сообщения и `@упоминания` и свои сообщения обратно не получает, но проверка не повредит.\n\n**Диспетчер событий:**\n\n```javascript\nasync function handleEvent(event) {\n  const { type, data } = event\n  const dialogId = data.dialogId\n\n  switch (type) {\n    case 'ONIMBOTV2MESSAGEADD': {\n      const m = data.message\n      if (m.isSystem || m.authorId === data.bot.id) return \u002F\u002F пропустить служебные и свои\n      \u002F\u002F Ответить: POST \u002Fv1\u002Fbots\u002F:botId\u002Fmessages с { dialogId, fields: { message } }\n      break\n    }\n    case 'ONIMBOTV2COMMANDADD':\n      \u002F\u002F Ответить на команду: POST \u002Fv1\u002Fbots\u002F:botId\u002Fcommands\u002F:commandId\u002Fanswer\n      \u002F\u002F commandId = data.command.id, messageId = data.message.id\n      break\n    case 'ONIMBOTV2JOINCHAT':\n      \u002F\u002F Бот добавлен в чат — можно отправить приветствие в dialogId\n      break\n    case 'ONIMBOTV2REACTIONCHANGE':\n      \u002F\u002F data.reaction (код реакции) + data.action ('add' | 'delete')\n      break\n    case 'ONIMBOTV2CONTEXTGET':\n      \u002F\u002F Диалог открыт по ссылке с контекстом — данные в data.context\n      break\n    case 'ONIMBOTV2MESSAGEUPDATE':\n      \u002F\u002F Сообщение отредактировано — data.message с новым текстом\n      break\n    case 'ONIMBOTV2MESSAGEDELETE':\n      \u002F\u002F Сообщение удалено — data.messageId (число)\n      break\n    case 'ONIMBOTV2DELETE':\n      \u002F\u002F Бот удалён с портала — освободите ресурсы, прекратите опрос\n      break\n  }\n}\n```\n\nОтвет в диалог — всегда [`POST \u002Fv1\u002Fbots\u002F:botId\u002Fmessages`](\u002Fdocs\u002Fbots\u002Fmessages\u002Fsend) с `dialogId = event.data.dialogId`.\n\n---\n\n## ONIMBOTV2MESSAGEADD\n\nНовое сообщение боту (личное или @упоминание в групповом чате).\n\n```json\n{\n  \"eventId\": 35,\n  \"type\": \"ONIMBOTV2MESSAGEADD\",\n  \"date\": \"2026-04-13T17:15:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"chat123\",\n    \"bot\": {\n      \"id\": 42,\n      \"code\": \"support_bot\",\n      \"type\": \"bot\",\n      \"isHidden\": false,\n      \"isReactionsEnabled\": true,\n      \"eventMode\": \"fetch\"\n    },\n    \"message\": {\n      \"id\": 1501,\n      \"chatId\": 123,\n      \"authorId\": 1,\n      \"date\": \"2026-04-13T17:15:00+03:00\",\n      \"text\": \"Привет, бот! Подскажи по задаче #42\",\n      \"isSystem\": false,\n      \"uuid\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n      \"forward\": null,\n      \"params\": {\n        \"FILE_ID\": [\"15423\"]\n      },\n      \"viewedByOthers\": false\n    },\n    \"chat\": {\n      \"id\": 123,\n      \"dialogId\": \"chat123\",\n      \"type\": \"chat\",\n      \"name\": \"Рабочий чат\",\n      \"owner\": 1,\n      \"color\": \"#64a513\",\n      \"entityType\": \"\",\n      \"entityId\": \"\",\n      \"permissions\": {\n        \"manageUsersAdd\": \"member\",\n        \"manageUsersDelete\": \"manager\",\n        \"manageSettings\": \"owner\",\n        \"manageMessages\": \"member\",\n        \"canPost\": \"member\"\n      }\n    },\n    \"user\": {\n      \"id\": 1,\n      \"active\": true,\n      \"name\": \"Иван Петров\",\n      \"firstName\": \"Иван\",\n      \"lastName\": \"Петров\",\n      \"workPosition\": \"Менеджер\",\n      \"color\": \"#1eb4aa\",\n      \"gender\": \"M\",\n      \"extranet\": false,\n      \"bot\": false,\n      \"status\": \"online\",\n      \"departments\": [1, 5],\n      \"type\": \"employee\"\n    },\n    \"language\": \"ru\"\n  }\n}\n```\n\n**Поля data:**\n\n| Поле | Тип | Описание |\n|------|-----|---------|\n| `dialogId` | string | ID диалога для ответа (`chatXXX` или id пользователя) |\n| `message.id` | number | ID сообщения — для цитирования через [replyId](\u002Fdocs\u002Fbots\u002Fmessages\u002Fsend) |\n| `message.text` | string | Текст сообщения |\n| `message.authorId` | number | ID автора. Сравните с `bot.id`, чтобы пропустить собственные сообщения |\n| `message.isSystem` | boolean | `true` для служебных сообщений — пропускайте их |\n| `message.params.FILE_ID` | string[] | ID прикреплённых файлов. Скачать: [GET \u002Ffiles\u002F:fileId](\u002Fdocs\u002Fbots\u002Ffiles\u002Fdownload) |\n| `message.forward` | object \\| null | Объект пересланного сообщения или `null` |\n| `bot` | object | Объект бота (`id`, `code`, `type`, `eventMode`) |\n| `chat.entityType` | string | Тип привязки: `LINES` (открытая линия), `CRM`, пустая строка (обычный чат) |\n| `chat.permissions` | object | Права в чате (`manageUsersAdd`, `manageSettings` и др.) |\n| `user` | object | Автор сообщения (`id`, `name`, `departments`, `status` и др.) |\n| `language` | string | Язык интерфейса пользователя |\n\n### Обработка голосовых и файловых сообщений\n\nГолосовые сообщения и файлы-вложения попадают в `ONIMBOTV2MESSAGEADD` со следующими особенностями:\n\n- У голосового сообщения `message.text` пуст или содержит только пояснение (`\"Голосовое сообщение\"`).\n- Массив `message.params.FILE_ID` — **авторитативный источник** информации о вложениях, появляется сразу в теле события.\n- `GET \u002Fv1\u002Fchats\u002F:dialogId\u002Fmessages` **не гарантирует**, что то же сообщение вернётся сразу — disk-индекс Битрикс24 иногда догоняет событие с задержкой в 1–2 секунды. Не ждите его через polling.\n\n**Корректный паттерн:**\n\n```javascript\nconst FILE_FETCH_TIMEOUT_MS = 10_000\n\nasync function handleMessageAdd(event) {\n  const { message } = event.data\n  const fileIds = message.params?.FILE_ID ?? []\n\n  \u002F\u002F Обычное текстовое сообщение\n  if (fileIds.length === 0) {\n    return handleText(message)\n  }\n\n  \u002F\u002F Есть вложение — читаем метаданные через disk API\n  for (const fileId of fileIds) {\n    let meta = null\n    for (let attempt = 0; attempt \u003C 2; attempt++) {\n      \u002F\u002F encodeURIComponent защищает от future-regressions если fileId когда-то\n      \u002F\u002F окажется не чисто-числовым (например, B24 поменяет формат ID или\n      \u002F\u002F этот паттерн расширят на user-provided ID из другого источника).\n      const url = `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Ffiles\u002F${encodeURIComponent(fileId)}`\n      \u002F\u002F AbortController защищает handler от зависания, если B24 или наш proxy\n      \u002F\u002F отвечают медленно. Без timeout одно «залипшее» событие может заблокировать poll-loop.\n      const controller = new AbortController()\n      const timer = setTimeout(() => controller.abort(), FILE_FETCH_TIMEOUT_MS)\n      let res\n      try {\n        res = await fetch(url, {\n          headers: { 'X-Api-Key': process.env.VIBE_KEY },\n          signal: controller.signal,\n        })\n      } catch (err) {\n        clearTimeout(timer)\n        if (err.name === 'AbortError') {\n          console.warn(`File ${fileId} fetch timed out after ${FILE_FETCH_TIMEOUT_MS}ms, retrying`)\n          continue\n        }\n        throw err\n      }\n      clearTimeout(timer)\n      if (res.ok) {\n        meta = await res.json()\n        break\n      }\n      \u002F\u002F 403 BITRIX_ACCESS_DENIED или 404 сразу после события — типично:\n      \u002F\u002F disk ещё не проиндексировал файл. Ждём 1.5 секунды и ретраим.\n      if (res.status === 403 || res.status === 404) {\n        await sleep(1500)\n        continue\n      }\n      throw new Error(`Failed to read file ${fileId}: HTTP ${res.status}`)\n    }\n    if (!meta) {\n      \u002F\u002F Файл всё ещё недоступен — логируем и продолжаем без attachment\n      console.warn(`File ${fileId} not available after retry, skipping`)\n      continue\n    }\n    await processAttachment(meta.data)\n  }\n}\n\nconst sleep = (ms) => new Promise((r) => setTimeout(r, ms))\n```\n\n**Требуемые скоупы у API-ключа:** `imbot` (получение событий) + **`disk`** (вызов `\u002Fv1\u002Ffiles\u002F:id`). Без скоупа `disk` запрос `GET \u002Fv1\u002Ffiles\u002F:id` возвращает `SCOPE_DENIED` 403. Скоуп `im` нужен дополнительно, если вы вызываете методы `im.*` напрямую (для этого паттерна не требуется).\n\n**Модель прав в Битрикс24:**\n\n- Бот-пользователь (от чьего имени работает API-ключ) должен быть **участником чата**, в котором пришло сообщение. Для групповых ботов участие добавляется автоматически при установке. Для ботов типа `personal` и `supervisor` требуется настройка в интерфейсе портала (см. [Зарегистрировать бота](\u002Fdocs\u002Fbots\u002Fmanagement\u002Fcreate)).\n- Файл-вложение доступно только в пределах срока хранения файлов в Битрикс24, который зависит от тарифа портала. После удаления файла запрос возвращает `BITRIX_ACCESS_DENIED` даже с корректным скоупом.\n- Скоуп `disk` у ключа **необходим, но недостаточен** — он открывает эндпоинт `\u002Fv1\u002Ffiles\u002F:id`, но права на конкретный файл Битрикс24 проверяет отдельно по участию бота в чате.\n\n**Расшифровка голосового сообщения.** Получив `fileId` из `message.params.FILE_ID`, скачайте байты файла и отправьте их на распознавание. Полная цепочка: событие → `fileId` → скачать содержимое → расшифровать.\n\n```javascript\nasync function transcribeVoice(fileId) {\n  \u002F\u002F 1. Скачать байты файла (бинарный ответ)\n  const fileRes = await fetch(\n    `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Ffiles\u002F${encodeURIComponent(fileId)}\u002Fdownload`,\n    { headers: { 'X-Api-Key': process.env.VIBE_KEY } },\n  )\n  const audio = await fileRes.blob()\n\n  \u002F\u002F 2. Отправить аудио на расшифровку (Whisper, тарифицируется по длительности в AI-квоте портала; скоуп `vibe:ai`)\n  const form = new FormData()\n  form.append('file', audio, 'voice.ogg')\n  const trRes = await fetch('https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Faudio\u002Ftranscriptions', {\n    method: 'POST',\n    headers: { 'X-Api-Key': process.env.VIBE_KEY },\n    body: form,\n  })\n  const { text } = await trRes.json()\n  return text\n}\n```\n\nКонтракт распознавания (форматы, лимиты, коды ошибок `empty_file` \u002F `AI_PROVIDER_TIMEOUT`) — [Расшифровка аудио](\u002Fdocs\u002Fai\u002Faudio). Скоуп `vibe:ai` добавляется к ключу автоматически.\n\n---\n\n## ONIMBOTV2MESSAGEUPDATE\n\nСообщение отредактировано. Структура `data` совпадает с `ONIMBOTV2MESSAGEADD`: объект `message` содержит обновлённый `text` и те же поля `id`, `authorId`, `params.FILE_ID`. Находите исходное сообщение по `message.id`.\n\n---\n\n## ONIMBOTV2MESSAGEDELETE\n\nСообщение удалено. Вместо объекта `message` содержит `messageId` (число).\n\n```json\n{\n  \"eventId\": 37,\n  \"type\": \"ONIMBOTV2MESSAGEDELETE\",\n  \"date\": \"2026-04-13T17:16:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"chat123\",\n    \"bot\": { \"id\": 42, \"code\": \"support_bot\", \"type\": \"bot\" },\n    \"messageId\": 1501,\n    \"chat\": { \"id\": 123, \"dialogId\": \"chat123\", \"type\": \"chat\", \"name\": \"Рабочий чат\" },\n    \"user\": { \"id\": 1, \"name\": \"Иван Петров\" }\n  }\n}\n```\n\n---\n\n## ONIMBOTV2JOINCHAT\n\nБот добавлен в чат. `user` — кто добавил бота.\n\n```json\n{\n  \"eventId\": 29,\n  \"type\": \"ONIMBOTV2JOINCHAT\",\n  \"date\": \"2026-04-13T17:12:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"chat3553\",\n    \"bot\": { \"id\": 42, \"code\": \"support_bot\", \"type\": \"bot\", \"eventMode\": \"fetch\" },\n    \"chat\": {\n      \"id\": 3553,\n      \"dialogId\": \"chat3553\",\n      \"type\": \"chat\",\n      \"name\": \"Отдел продаж\",\n      \"owner\": 42,\n      \"color\": \"#64a513\",\n      \"permissions\": { \"manageUsersAdd\": \"member\", \"manageSettings\": \"owner\" }\n    },\n    \"user\": { \"id\": 3, \"name\": \"Мария Сидорова\", \"workPosition\": \"Руководитель\" },\n    \"language\": \"ru\"\n  }\n}\n```\n\n---\n\n## ONIMBOTV2COMMANDADD\n\nВызвана slash-команда бота. Содержит дополнительный объект `command`.\n\n```json\n{\n  \"eventId\": 40,\n  \"type\": \"ONIMBOTV2COMMANDADD\",\n  \"date\": \"2026-04-13T17:18:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"chat123\",\n    \"bot\": { \"id\": 42, \"code\": \"support_bot\", \"type\": \"bot\" },\n    \"message\": { \"id\": 1510, \"text\": \"\u002Fhelp задачи\" },\n    \"chat\": { \"id\": 123, \"dialogId\": \"chat123\" },\n    \"user\": { \"id\": 1, \"name\": \"Иван Петров\" },\n    \"command\": {\n      \"id\": 7,\n      \"command\": \"help\",\n      \"params\": \"задачи\",\n      \"context\": \"textarea\"\n    }\n  }\n}\n```\n\n**Поля command:**\n\n| Поле | Описание |\n|------|---------|\n| `id` | ID команды |\n| `command` | Текст команды без `\u002F` |\n| `params` | Параметры, введённые после команды |\n| `context` | Откуда вызвана: `textarea` (поле ввода), `keyboard` (кнопка), `menu` (контекстное меню) |\n\nДля ответа используйте [POST \u002Fcommands\u002F:commandId\u002Fanswer](\u002Fdocs\u002Fbots\u002Fcommands\u002Fanswer) с `messageId` из `message.id`.\n\n---\n\n## ONIMBOTV2REACTIONCHANGE\n\nРеакция добавлена или удалена на сообщение бота.\n\n```json\n{\n  \"eventId\": 42,\n  \"type\": \"ONIMBOTV2REACTIONCHANGE\",\n  \"date\": \"2026-04-13T17:19:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"chat123\",\n    \"bot\": { \"id\": 42, \"code\": \"support_bot\", \"type\": \"bot\" },\n    \"reaction\": \"like\",\n    \"action\": \"add\",\n    \"message\": { \"id\": 1502, \"text\": \"Привет! Чем могу помочь?\" },\n    \"chat\": { \"id\": 123, \"dialogId\": \"chat123\" },\n    \"user\": { \"id\": 1, \"name\": \"Иван Петров\" }\n  }\n}\n```\n\n| Поле | Описание |\n|------|---------|\n| `reaction` | Код реакции (см. [коды реакций](\u002Fdocs\u002Fbots\u002Fui\u002Freactions)) |\n| `action` | `\"add\"` — добавлена, `\"delete\"` — удалена |\n\n---\n\n## ONIMBOTV2DELETE\n\nБот удалён с портала. Содержит только объект `bot` — без `chat` и `user`.\n\n```json\n{\n  \"eventId\": 50,\n  \"type\": \"ONIMBOTV2DELETE\",\n  \"date\": \"2026-04-13T17:25:00+03:00\",\n  \"data\": {\n    \"bot\": { \"id\": 42, \"code\": \"support_bot\", \"type\": \"bot\" }\n  }\n}\n```\n\n---\n\n## ONIMBOTV2CONTEXTGET\n\nПользователь открыл диалог с ботом по ссылке, в которую вложен контекст. В поле `context` приходят произвольные данные из этой ссылки — бот может сразу ответить с их учётом.\n\n```json\n{\n  \"eventId\": 45,\n  \"type\": \"ONIMBOTV2CONTEXTGET\",\n  \"date\": \"2026-04-13T17:20:00+03:00\",\n  \"data\": {\n    \"dialogId\": \"5\",\n    \"bot\": { \"id\": 42, \"code\": \"support_bot\", \"type\": \"bot\" },\n    \"context\": { \"action\": \"openTask\", \"taskId\": \"456\" },\n    \"chat\": { \"id\": 789, \"dialogId\": \"5\", \"type\": \"private\" },\n    \"user\": { \"id\": 5, \"name\": \"Алексей Козлов\" }\n  }\n}\n```\n\n| Поле | Описание |\n|------|---------|\n| `context` | Произвольные данные из ссылки, по которой открыли диалог. Приходит строкой или объектом. Битрикс24 передаёт значения строками — число `456` придёт как `\"456\"` |\n\nСобытие приходит для личного диалога с ботом, поэтому `dialogId` — это id пользователя, открывшего ссылку.\n\n**Как боту передают контекст.** Диалог с ботом открывают по ссылке с параметром `BOT_CONTEXT` — в него кладут произвольный JSON в URL-кодировке:\n\n```\nhttps:\u002F\u002F\u003Cportal>\u002Fonline\u002F?IM_DIALOG=\u003CdialogId>&BOT_CONTEXT=\u003CJSON в URL-кодировке>\n```\n\n`\u003Cportal>` — домен портала, `\u003CdialogId>` — идентификатор диалога с ботом. Значение `BOT_CONTEXT` приходит боту в поле `context` этого события без изменений.\n\n---\n\n## Общие объекты в событиях\n\nВсе события (кроме `ONIMBOTV2DELETE`) содержат объекты `bot`, `chat`, `user` с одинаковой структурой. Полные объекты показаны в примере `ONIMBOTV2MESSAGEADD` выше. В сокращённых примерах показаны только ключевые поля.\n\n## Смотрите также\n\n- [Получить события](\u002Fdocs\u002Fbots\u002Fevents\u002Fpolling)\n- [User-события](\u002Fdocs\u002Fbots\u002Fevents\u002Fuser-events)\n- [Отправить сообщение](\u002Fdocs\u002Fbots\u002Fmessages\u002Fsend)\n- [Ответить на команду](\u002Fdocs\u002Fbots\u002Fcommands\u002Fanswer)\n- [Коды реакций](\u002Fdocs\u002Fbots\u002Fui\u002Freactions)\n- [Бот-платформа](\u002Fdocs\u002Fbots)\n","\n## User-события (ONIMV2*)\n\nОбщий первый шаг — подписка: [`POST \u002Fv1\u002Fchats\u002Fevents\u002Fsubscribe`](\u002Fdocs\u002Fchats\u002Fevents\u002Fsubscribe) (скоуп `im`, не `imbot`). Без активной подписки Битрикс24 не регистрирует пользовательские события.\n\nДальше события забирают одним из двух способов:\n\n1. **Вместе с bot-событиями** — передать `withUserEvents=true` в [polling бота](\u002Fdocs\u002Fbots\u002Fevents\u002Fpolling). Bot-события (`ONIMBOTV2*`) и user-события (`ONIMV2*`) приходят в одном массиве `events`.\n2. **Отдельным опросом от имени пользователя** — [`GET \u002Fv1\u002Fchats\u002Fevents`](\u002Fdocs\u002Fchats\u002Fevents\u002Fpoll). Не требует зарегистрированного бота, подходит для агентов, работающих от имени пользователя.\n\n## Список событий\n\n| Событие | Описание |\n|---------|---------|\n| `ONIMV2MESSAGEADD` | Новое сообщение в подписанном чате |\n| `ONIMV2MESSAGEUPDATE` | Сообщение отредактировано |\n| `ONIMV2MESSAGEDELETE` | Сообщение удалено |\n| `ONIMV2JOINCHAT` | Участник зашёл в чат |\n| `ONIMV2REACTIONCHANGE` | Реакция изменена |\n\n## Известные особенности\n\n**Отдельный скоуп:** [подписка](\u002Fdocs\u002Fchats\u002Fevents\u002Fsubscribe) и [отписка](\u002Fdocs\u002Fchats\u002Fevents\u002Funsubscribe) требуют скоуп `im`, а не `imbot`.\n\n## Смотрите также\n\n- [Получить события](\u002Fdocs\u002Fbots\u002Fevents\u002Fpolling)\n- [Получить пользовательские события](\u002Fdocs\u002Fchats\u002Fevents\u002Fpoll)\n- [Bot-события](\u002Fdocs\u002Fbots\u002Fevents\u002Fbot-events)\n- [Бот-платформа](\u002Fdocs\u002Fbots)\n"]