
## Bot events (ONIMBOTV2*)

Arrive automatically for all bots with `eventMode: "fetch"`. Received via [polling](/docs/bots/events/polling).

Every event has the structure:

```json
{
  "eventId": 29,
  "type": "ONIMBOTV2JOINCHAT",
  "date": "2026-04-13T17:12:00+00:00",
  "data": {
    "dialogId": "chat3553",
    "bot": { ... },
    "chat": { ... },
    "user": { ... },
    "language": "en"
  }
}
```

| Field | Type | Description |
|------|-----|---------|
| `eventId` | number | Event ID — pass it as `offset` in the next polling request |
| `type` | string | Event type code |
| `date` | string | Date and time (ISO 8601) |
| `data` | object | Event data (structure depends on the type) |

## Event list

| Type | Description |
|-----|---------|
| [ONIMBOTV2MESSAGEADD](#onimbotv2messageadd) | New message to the bot |
| [ONIMBOTV2MESSAGEUPDATE](#onimbotv2messageupdate) | Message edited |
| [ONIMBOTV2MESSAGEDELETE](#onimbotv2messagedelete) | Message deleted |
| [ONIMBOTV2JOINCHAT](#onimbotv2joinchat) | Bot added to a chat |
| [ONIMBOTV2COMMANDADD](#onimbotv2commandadd) | A slash command was invoked |
| [ONIMBOTV2REACTIONCHANGE](#onimbotv2reactionchange) | Reaction on a bot message |
| [ONIMBOTV2DELETE](#onimbotv2delete) | Bot removed from the Bitrix24 account |
| [ONIMBOTV2CONTEXTGET](#onimbotv2contextget) | Dialog context requested |

---

## How to handle events

The polling loop is described on the [Get events](/docs/bots/events/polling) page. Once you receive the `events` array, route each event by its `type` field. You must reply to the bot in the `event.data.dialogId` dialog (for groups — `chatXXX`, for direct messages — the user id, the same as `event.data.chat.dialogId`).

**Before processing a message, filter out the noise:**

- `message.isSystem === true` — a system message (joining a chat, settings change). Skip it.
- `message.authorId === event.data.bot.id` — the bot's own message. Bots of type `personal` and `supervisor` receive all chat messages, including their own replies — without this check the handler will loop forever. A bot of type `bot` receives only direct messages and `@mentions` and does not get its own messages back, but the check does no harm.

**Event dispatcher:**

```javascript
async function handleEvent(event) {
  const { type, data } = event
  const dialogId = data.dialogId

  switch (type) {
    case 'ONIMBOTV2MESSAGEADD': {
      const m = data.message
      if (m.isSystem || m.authorId === data.bot.id) return // skip system and own messages
      // Reply: POST /v1/bots/:botId/messages with { dialogId, fields: { message } }
      break
    }
    case 'ONIMBOTV2COMMANDADD':
      // Reply to the command: POST /v1/bots/:botId/commands/:commandId/answer
      // commandId = data.command.id, messageId = data.message.id
      break
    case 'ONIMBOTV2JOINCHAT':
      // Bot added to a chat — you can send a greeting to dialogId
      break
    case 'ONIMBOTV2REACTIONCHANGE':
      // data.reaction (reaction code) + data.action ('add' | 'delete')
      break
    case 'ONIMBOTV2CONTEXTGET':
      // Dialog opened via a link with context — data in data.context
      break
    case 'ONIMBOTV2MESSAGEUPDATE':
      // Message edited — data.message with the new text
      break
    case 'ONIMBOTV2MESSAGEDELETE':
      // Message deleted — data.messageId (a number)
      break
    case 'ONIMBOTV2DELETE':
      // Bot removed from the Bitrix24 account — release resources, stop polling
      break
  }
}
```

A reply to a dialog is always [`POST /v1/bots/:botId/messages`](/docs/bots/messages/send) with `dialogId = event.data.dialogId`.

---

## ONIMBOTV2MESSAGEADD

New message to the bot (direct or an @mention in a group chat).

```json
{
  "eventId": 35,
  "type": "ONIMBOTV2MESSAGEADD",
  "date": "2026-04-13T17:15:00+00:00",
  "data": {
    "dialogId": "chat123",
    "bot": {
      "id": 42,
      "code": "support_bot",
      "type": "bot",
      "isHidden": false,
      "isReactionsEnabled": true,
      "eventMode": "fetch"
    },
    "message": {
      "id": 1501,
      "chatId": 123,
      "authorId": 1,
      "date": "2026-04-13T17:15:00+00:00",
      "text": "Hi, bot! Help me with task #42",
      "isSystem": false,
      "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "forward": null,
      "params": {
        "FILE_ID": ["15423"],
        "REPLY_ID": "1498"
      },
      "viewedByOthers": false
    },
    "chat": {
      "id": 123,
      "dialogId": "chat123",
      "type": "chat",
      "name": "Work chat",
      "owner": 1,
      "color": "#64a513",
      "entityType": "",
      "entityId": "",
      "permissions": {
        "manageUsersAdd": "member",
        "manageUsersDelete": "manager",
        "manageSettings": "owner",
        "manageMessages": "member",
        "canPost": "member"
      }
    },
    "user": {
      "id": 1,
      "active": true,
      "name": "John Brown",
      "firstName": "John",
      "lastName": "Brown",
      "workPosition": "Manager",
      "color": "#1eb4aa",
      "gender": "M",
      "extranet": false,
      "bot": false,
      "status": "online",
      "departments": [1, 5],
      "type": "employee"
    },
    "language": "en"
  }
}
```

**data fields:**

| Field | Type | Description |
|------|-----|---------|
| `dialogId` | string | Dialog ID for the reply (`chatXXX` or the user id) |
| `message.id` | number | Message ID — for quoting via [replyId](/docs/bots/messages/send) |
| `message.text` | string | Message text |
| `message.authorId` | number | Author ID. Compare with `bot.id` to skip your own messages |
| `message.isSystem` | boolean | `true` for system messages — skip them |
| `message.params.FILE_ID` | string[] | IDs of attached files. Download: [GET /files/:fileId](/docs/bots/files/download) |
| `message.params.REPLY_ID` | string | Reply to a message: the ID of the quoted message. Read the original: [GET /messages/:messageId](/docs/bots/messages/get) for `personal`/`supervisor` bots, otherwise [GET /chats/:dialogId/messages](/docs/chats/messages/list) |
| `message.forward` | object \| null | Forwarded message object or `null` |
| `bot` | object | Bot object (`id`, `code`, `type`, `eventMode`) |
| `chat.entityType` | string | Binding type: `LINES` (Open Channels), `CRM`, empty string (regular chat) |
| `chat.permissions` | object | Chat permissions (`manageUsersAdd`, `manageSettings`, etc.) |
| `user` | object | Message author (`id`, `name`, `departments`, `status`, etc.) |
| `language` | string | User interface language |

### Handling voice and file messages

Voice messages and file attachments arrive in `ONIMBOTV2MESSAGEADD` with the following specifics:

- A voice message has `message.text` empty or containing only a note (`"Voice message"`).
- The `message.params.FILE_ID` array is the **authoritative source** of attachment information; it appears in the event payload immediately.
- `GET /v1/chats/:dialogId/messages` **does not guarantee** that the same message returns right away — the Bitrix24 disk index sometimes catches up to the event with a 1–2 second delay. Do not wait for it via polling.

**Correct pattern:**

```javascript
const FILE_FETCH_TIMEOUT_MS = 10_000

async function handleMessageAdd(event) {
  const { message } = event.data
  const fileIds = message.params?.FILE_ID ?? []

  // Regular text message
  if (fileIds.length === 0) {
    return handleText(message)
  }

  // There is an attachment — read metadata via the disk API
  for (const fileId of fileIds) {
    let meta = null
    for (let attempt = 0; attempt < 2; attempt++) {
      // encodeURIComponent guards against future regressions if fileId ever
      // turns out not to be purely numeric (for example, B24 changes the ID format or
      // this pattern is extended to a user-provided ID from another source).
      const url = `https://vibecode.bitrix24.com/v1/files/${encodeURIComponent(fileId)}`
      // AbortController protects the handler from hanging if B24 or our proxy
      // respond slowly. Without a timeout one "stuck" event can block the poll loop.
      const controller = new AbortController()
      const timer = setTimeout(() => controller.abort(), FILE_FETCH_TIMEOUT_MS)
      let res
      try {
        res = await fetch(url, {
          headers: { 'X-Api-Key': process.env.VIBE_KEY },
          signal: controller.signal,
        })
      } catch (err) {
        clearTimeout(timer)
        if (err.name === 'AbortError') {
          console.warn(`File ${fileId} fetch timed out after ${FILE_FETCH_TIMEOUT_MS}ms, retrying`)
          continue
        }
        throw err
      }
      clearTimeout(timer)
      if (res.ok) {
        meta = await res.json()
        break
      }
      // 403 BITRIX_ACCESS_DENIED or 404 right after the event is typical:
      // the disk has not indexed the file yet. Wait 1.5 seconds and retry.
      if (res.status === 403 || res.status === 404) {
        await sleep(1500)
        continue
      }
      throw new Error(`Failed to read file ${fileId}: HTTP ${res.status}`)
    }
    if (!meta) {
      // The file is still unavailable — log it and continue without the attachment
      console.warn(`File ${fileId} not available after retry, skipping`)
      continue
    }
    await processAttachment(meta.data)
  }
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
```

**Required scopes on the API key:** `imbot` (receiving events) + **`disk`** (calling `/v1/files/:id`). Without the `disk` scope, a `GET /v1/files/:id` request returns `SCOPE_DENIED` 403. The `im` scope is additionally needed if you call `im.*` methods directly (not required for this pattern).

**Permission model in Bitrix24:**

- The bot user (on whose behalf the API key works) must be a **chat participant** in the chat where the message arrived. For group bots, participation is added automatically on installation. For bots of type `personal` and `supervisor`, setup in your Bitrix24 account is required (see [Register a bot](/docs/bots/management/create)).
- A file attachment is available only within the Bitrix24 file retention period, which depends on the Bitrix24 account plan. After a file is deleted, the request returns `BITRIX_ACCESS_DENIED` even with the correct scope.
- The `disk` scope on the key is **necessary but not sufficient** — it opens the `/v1/files/:id` endpoint, but Bitrix24 checks permission for a specific file separately by the bot's chat membership.

**Transcribing a voice message.** After getting the `fileId` from `message.params.FILE_ID`, download the file bytes and send them for recognition. Full chain: event → `fileId` → download the content → transcribe.

```javascript
async function transcribeVoice(fileId) {
  // 1. Download the file bytes (binary response)
  const fileRes = await fetch(
    `https://vibecode.bitrix24.com/v1/files/${encodeURIComponent(fileId)}/download`,
    { headers: { 'X-Api-Key': process.env.VIBE_KEY } },
  )
  const audio = await fileRes.blob()

  // 2. Send the audio for transcription (Whisper, billed by duration against the portal AI quota; scope `vibe:ai`)
  const form = new FormData()
  form.append('file', audio, 'voice.ogg')
  const trRes = await fetch('https://vibecode.bitrix24.com/v1/audio/transcriptions', {
    method: 'POST',
    headers: { 'X-Api-Key': process.env.VIBE_KEY },
    body: form,
  })
  const { text } = await trRes.json()
  return text
}
```

The recognition contract (formats, limits, error codes `empty_file` / `AI_PROVIDER_TIMEOUT`) — [Audio transcription](/docs/ai/audio). The `vibe:ai` scope is added to the key automatically.

---

## ONIMBOTV2MESSAGEUPDATE

Message edited. The `data` structure matches `ONIMBOTV2MESSAGEADD`: the `message` object contains the updated `text` and the same `id`, `authorId`, `params.FILE_ID`, `params.REPLY_ID` fields. Find the original message by `message.id`.

---

## ONIMBOTV2MESSAGEDELETE

Message deleted. Instead of a `message` object it contains `messageId` (a number).

```json
{
  "eventId": 37,
  "type": "ONIMBOTV2MESSAGEDELETE",
  "date": "2026-04-13T17:16:00+00:00",
  "data": {
    "dialogId": "chat123",
    "bot": { "id": 42, "code": "support_bot", "type": "bot" },
    "messageId": 1501,
    "chat": { "id": 123, "dialogId": "chat123", "type": "chat", "name": "Work chat" },
    "user": { "id": 1, "name": "John Brown" }
  }
}
```

---

## ONIMBOTV2JOINCHAT

Bot added to a chat. `user` — who added the bot.

```json
{
  "eventId": 29,
  "type": "ONIMBOTV2JOINCHAT",
  "date": "2026-04-13T17:12:00+00:00",
  "data": {
    "dialogId": "chat3553",
    "bot": { "id": 42, "code": "support_bot", "type": "bot", "eventMode": "fetch" },
    "chat": {
      "id": 3553,
      "dialogId": "chat3553",
      "type": "chat",
      "name": "Sales department",
      "owner": 42,
      "color": "#64a513",
      "permissions": { "manageUsersAdd": "member", "manageSettings": "owner" }
    },
    "user": { "id": 3, "name": "Maria Davis", "workPosition": "Head" },
    "language": "en"
  }
}
```

---

## ONIMBOTV2COMMANDADD

A bot slash command was invoked. Contains an additional `command` object.

```json
{
  "eventId": 40,
  "type": "ONIMBOTV2COMMANDADD",
  "date": "2026-04-13T17:18:00+00:00",
  "data": {
    "dialogId": "chat123",
    "bot": { "id": 42, "code": "support_bot", "type": "bot" },
    "message": { "id": 1510, "text": "/help tasks" },
    "chat": { "id": 123, "dialogId": "chat123" },
    "user": { "id": 1, "name": "John Brown" },
    "command": {
      "id": 7,
      "command": "help",
      "params": "tasks",
      "context": "textarea"
    }
  }
}
```

**command fields:**

| Field | Description |
|------|---------|
| `id` | Command ID |
| `command` | Command text without `/` |
| `params` | Parameters entered after the command |
| `context` | Where it was invoked from: `textarea` (input field), `keyboard` (button), `menu` (context menu) |

To reply, use [POST /commands/:commandId/answer](/docs/bots/commands/answer) with `messageId` from `message.id`.

---

## ONIMBOTV2REACTIONCHANGE

A reaction was added or removed on a bot message.

```json
{
  "eventId": 42,
  "type": "ONIMBOTV2REACTIONCHANGE",
  "date": "2026-04-13T17:19:00+00:00",
  "data": {
    "dialogId": "chat123",
    "bot": { "id": 42, "code": "support_bot", "type": "bot" },
    "reaction": "like",
    "action": "add",
    "message": { "id": 1502, "text": "Hi! How can I help?" },
    "chat": { "id": 123, "dialogId": "chat123" },
    "user": { "id": 1, "name": "John Brown" }
  }
}
```

| Field | Description |
|------|---------|
| `reaction` | Reaction code (see [reaction codes](/docs/bots/ui/reactions)) |
| `action` | `"add"` — added, `"delete"` — removed |

---

## ONIMBOTV2DELETE

Bot removed from the Bitrix24 account. Contains only the `bot` object — without `chat` and `user`.

```json
{
  "eventId": 50,
  "type": "ONIMBOTV2DELETE",
  "date": "2026-04-13T17:25:00+00:00",
  "data": {
    "bot": { "id": 42, "code": "support_bot", "type": "bot" }
  }
}
```

---

## ONIMBOTV2CONTEXTGET

A user opened a dialog with the bot via a link that carries embedded context. The `context` field receives the arbitrary data from that link — the bot can reply immediately taking it into account.

```json
{
  "eventId": 45,
  "type": "ONIMBOTV2CONTEXTGET",
  "date": "2026-04-13T17:20:00+00:00",
  "data": {
    "dialogId": "5",
    "bot": { "id": 42, "code": "support_bot", "type": "bot" },
    "context": { "action": "openTask", "taskId": "456" },
    "chat": { "id": 789, "dialogId": "5", "type": "private" },
    "user": { "id": 5, "name": "Alex Hughes" }
  }
}
```

| Field | Description |
|------|---------|
| `context` | Arbitrary data from the link the dialog was opened with. Arrives as a string or an object. Bitrix24 passes values as strings — the number `456` arrives as `"456"` |

The event arrives for a direct dialog with the bot, so `dialogId` is the id of the user who opened the link.

**How context is passed to the bot.** A dialog with the bot is opened via a link with the `BOT_CONTEXT` parameter — it carries arbitrary URL-encoded JSON:

```
https://<portal>/online/?IM_DIALOG=<dialogId>&BOT_CONTEXT=<URL-encoded JSON>
```

`<portal>` is the Bitrix24 account domain, `<dialogId>` is the identifier of the dialog with the bot. The `BOT_CONTEXT` value arrives to the bot in the `context` field of this event unchanged.

---

## Common objects in events

All events (except `ONIMBOTV2DELETE`) contain `bot`, `chat`, `user` objects with the same structure. The full objects are shown in the `ONIMBOTV2MESSAGEADD` example above. The abbreviated examples show only the key fields.

## See also

- [Get events](/docs/bots/events/polling)
- [User events](/docs/bots/events/user-events)
- [Send a message](/docs/bots/messages/send)
- [Reply to a command](/docs/bots/commands/answer)
- [Reaction codes](/docs/bots/ui/reactions)
- [Bot platform](/docs/bots)
