
## Получить события (polling)

`GET /v1/bots/:botId/events`

Основной механизм получения входящих сообщений и команд. Бот периодически запрашивает новые события.

Вайбкод хранит `lastOffset` в базе данных — при первом запросе без `offset` используется сохранённое значение. Это позволяет боту продолжить с места остановки после перезапуска.

## Параметры

| Параметр | Тип | Обяз. | По умолч. | Описание |
|----------|-----|:-----:|-----------|---------|
| `botId` (path) | number | да | — | ID бота |
| `offset` (query) | number | нет | из БД | Начальная позиция. Без параметра — сохранённое в БД значение. `offset=0` — начать с начала |
| `limit` (query) | number | нет | `100` | Максимальное количество событий (1-1000) |
| `withUserEvents` (query) | boolean | нет | `false` | Включить [user-события](/docs/bots/events/user-events) (ONIMV2*). Требует предварительной подписки — порядок настройки описан на странице User-события. Без подписки запрос вернёт `422 BITRIX_ERROR: User is not subscribed` |

## Примеры

### curl — личный ключ

```bash
curl "https://vibecode.bitrix24.tech/v1/bots/42/events?limit=50" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth-приложение

```bash
curl "https://vibecode.bitrix24.tech/v1/bots/42/events?limit=50" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — личный ключ

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/bots/42/events?limit=50', {
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
  },
})

const { success, data } = await res.json()
console.log('События:', data.events.length, 'Ещё:', data.hasMore)
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/bots/42/events?limit=50', {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})

const { success, data } = await res.json()
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|---------|
| `events` | array | Массив событий (см. [типы событий](/docs/bots/events/bot-events)) |
| `events[].eventId` | number | ID события — передайте как `offset` в следующем запросе |
| `events[].type` | string | Код события (`ONIMBOTV2MESSAGEADD`, `ONIMBOTV2COMMANDADD` и т.д.) |
| `events[].date` | string | Дата и время события (ISO 8601) |
| `events[].data` | object | Данные события (структура зависит от типа, ключи в camelCase) |
| `nextOffset` | number | Смещение для следующего запроса |
| `hasMore` | boolean | Есть ещё необработанные события |
| `storedOffset` | number | Текущее сохранённое смещение в БД |
| `persisted` | boolean | `true` если `lastOffset` в БД продвинулся (доставлено хотя бы одно событие). `false` — ответ пустой, курсор не изменился |
| `nextPollAfterMs` | number | Сколько миллисекунд подождать перед следующим запросом. Приходит только боту, которому события доставляются вебхуком (`eventMode` равен `webhook`) — очередь `Event.get` у такого бота пуста по устройству Битрикс24. Боту в режиме `fetch` поле не приходит: отсутствие означает «опрашивайте с прежней частотой», поэтому пустым или нулевым оно не бывает. Если `hasMore` равен `true`, дочитайте очередь не дожидаясь паузы — она относится к следующему пустому опросу |
| `hint` | string | Диагностическое сообщение при устойчиво пустой очереди — указывает на состояние установки на стороне Битрикс24. Счётчик пустых ответов обновляется периодически, а не на каждый запрос, поэтому подсказка появляется после нескольких периодов пустоты, а не строго на пятом запросе (при рекомендованном интервале опроса 2–5 секунд — спустя примерно пару минут непрерывно пустого опроса). Число N в тексте — количество зафиксированных периодов пустоты, а не точное число сделанных запросов. Для ветвления в коде опирайтесь на `persisted` и наличие событий, а `hint` трактуйте как подсказку |

## Пример ответа

Есть новые события (`persisted: true`):

```json
{
  "success": true,
  "data": {
    "events": [
      {
        "eventId": 35,
        "type": "ONIMBOTV2MESSAGEADD",
        "date": "2026-04-13T17:15:00+03:00",
        "data": {
          "dialogId": "chat123",
          "message": { "id": 1501, "text": "Привет, бот!" },
          "user": { "id": 1, "name": "Иван Петров" }
        }
      }
    ],
    "nextOffset": 36,
    "hasMore": false,
    "storedOffset": 35,
    "persisted": true
  }
}
```

Устойчиво пустая очередь событий (появляется поле `hint`, число в тексте — количество зафиксированных периодов пустоты, а не сделанных запросов):

```json
{
  "success": true,
  "data": {
    "events": [],
    "nextOffset": 36,
    "hasMore": false,
    "storedOffset": 36,
    "persisted": false,
    "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 /v1/bots/42/resubscribe first (lightweight, preserves openline / 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 /v1/bots to re-register (destroys openline bindings; last resort)."
  }
}
```

## Пример ответа при ошибке

404 — бот не найден:

```json
{
  "success": false,
  "error": {
    "code": "BOT_NOT_FOUND",
    "message": "Bot 999 not found. Register it first via POST /v1/bots."
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|---------|
| 400 | `INVALID_BOT_ID` | `botId` не является числом |
| 404 | `BOT_NOT_FOUND` | Бот с таким ID не найден |
| 403 | `BOT_ACCESS_DENIED` | Бот принадлежит другому API-ключу |
| 403 | `B24_MARKET_SUBSCRIPTION_REQUIRED` | Подписка Маркетплейса неактивна. Немедленно остановите polling; порядок восстановления описан ниже |
| 422 | `BITRIX_ERROR` | Ошибка Битрикс24 (текст ошибки в `message`) |
| 403 | `SCOPE_DENIED` | API-ключ не имеет скоупа `imbot` |
| 401 | `TOKEN_MISSING` | API-ключ не имеет настроенных токенов |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Какой токен использовать.** Для личного ключа `vibe_api_…` достаточно заголовка `X-Api-Key`. Для ключа авторизации `vibe_app_…` обязательно добавлять `Authorization: Bearer <session_token>` — без Bearer запрос вернёт `401 TOKEN_MISSING`. Получение `session_token` для OAuth-приложения — см. [Ключи и авторизация](/docs/keys-auth).

**Неактивная подписка Маркетплейса.** `B24_MARKET_SUBSCRIPTION_REQUIRED` не является временной
ошибкой polling: несмотря на `Retry-After: 3600`, поле `error.details.retryable` равно `false`.
Остановите цикл. Подробная форма ответа и восстановление — [ошибки подписки](/docs/errors/billing).

**Серверное хранение offset:** Вайбкод хранит `lastOffset` в базе. При первом запросе без `offset` — используется сохранённое значение. После получения событий `lastOffset` обновляется автоматически, не дожидаясь результата записи.

**offset=0:** явная передача `offset=0` начинает с начала истории — для отладки или первичной загрузки.

**offset=N (конкретное значение):** само событие с указанным ID попадает в ответ. Чтобы не получить дубли, всегда передавайте `nextOffset` из предыдущего ответа, а не `eventId` последнего обработанного события.

**Цепочка запросов:**

```
GET /events           → { nextOffset: 42, hasMore: true }
GET /events?offset=42 → { nextOffset: 55, hasMore: false }
GET /events?offset=55 → { events: [], hasMore: false }
```

**Рекомендуемый интервал polling:** 2-5 секунд между запросами — если платформа не прислала `nextPollAfterMs`. Прислала — ждите столько, сколько там указано: значение может меняться между релизами платформы, поэтому зажимайте его своими границами, а не полагайтесь на конкретное число.

**Опрос не удерживает машину в сети.** Если цикл ниже работает на сервере Black Hole, отключите на нём авто-сон. Таймер простоя считает **входящие** обращения к серверу, а опрос делает только исходящие вызовы — с точки зрения таймера сервер простаивает и через час засыпает вместе с процессом бота. Отключается вызовом [`PATCH /v1/infra/servers/:id/sleep`](/docs/infra/lifecycle/sleep) со значением `sleepAfterMinutes: null`.

**Polling-цикл (готовый пример):**

```javascript
const BOT_ID = 42
const API_KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.tech/v1'

async function pollEvents() {
  let offset = undefined

  while (true) {
    try {
      const url = new URL(`${BASE}/bots/${BOT_ID}/events`)
      if (offset !== undefined) url.searchParams.set('offset', String(offset))

      const res = await fetch(url, {
        headers: { 'X-Api-Key': API_KEY },
      })
      const { data } = await res.json()

      for (const event of data.events ?? []) {
        await handleEvent(event)
      }

      if (data.nextOffset !== undefined) {
        offset = data.nextOffset
      }
    } catch (err) {
      console.error('Poll error:', err.message)
    }

    // Платформа может попросить опрашивать реже (`nextPollAfterMs`); поля нет — свой интервал.
    await new Promise(r => setTimeout(r, Math.min(data?.nextPollAfterMs ?? 3000, 3600000)))
  }
}

async function handleEvent(event) {
  const { data } = event

  switch (event.type) {
    case 'ONIMBOTV2MESSAGEADD':
      // Ответить на сообщение
      await fetch(`${BASE}/bots/${BOT_ID}/messages`, {
        method: 'POST',
        headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          dialogId: data.chat.dialogId,
          fields: { message: `Получил: ${data.message.text}` },
        }),
      })
      break

    case 'ONIMBOTV2COMMANDADD':
      // Ответить на команду
      await fetch(`${BASE}/bots/${BOT_ID}/commands/${data.command.id}/answer`, {
        method: 'POST',
        headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          dialogId: data.chat.dialogId,
          messageId: data.message.id,
          fields: { message: `Команда /${data.command.command}: ${data.command.params}` },
        }),
      })
      break
  }
}

pollEvents()
```

## Смотрите также

- [Диагностика проблем](/docs/bots/troubleshooting)
- [Bot-события](/docs/bots/events/bot-events)
- [User-события](/docs/bots/events/user-events)
- [Отправить сообщение](/docs/bots/messages/send)
- [Ответить на команду](/docs/bots/commands/answer)
- [Настроить авто-сон](/docs/infra/lifecycle/sleep)
- [Бот-платформа](/docs/bots)
- [Лимиты и оптимизация](/docs/optimization)
