For AI agents: markdown of this page — /docs-content-en/bots/events/bot-events.md documentation index — /llms.txt
Bot events (ONIMBOTV2*)
Arrive automatically for all bots with eventMode: "fetch". Received via polling.
Every event has the structure:
{
"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 | New message to the bot |
| ONIMBOTV2MESSAGEUPDATE | Message edited |
| ONIMBOTV2MESSAGEDELETE | Message deleted |
| ONIMBOTV2JOINCHAT | Bot added to a chat |
| ONIMBOTV2COMMANDADD | A slash command was invoked |
| ONIMBOTV2REACTIONCHANGE | Reaction on a bot message |
| ONIMBOTV2DELETE | Bot removed from the Bitrix24 account |
| ONIMBOTV2CONTEXTGET | Dialog context requested |
How to handle events
The polling loop is described on the Get events 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 typepersonalandsupervisorreceive all chat messages, including their own replies — without this check the handler will loop forever. A bot of typebotreceives only direct messages and@mentionsand does not get its own messages back, but the check does no harm.
Event dispatcher:
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 with dialogId = event.data.dialogId.
ONIMBOTV2MESSAGEADD
New message to the bot (direct or an @mention in a group chat).
{
"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"]
},
"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 |
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 |
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.textempty or containing only a note ("Voice message"). - The
message.params.FILE_IDarray is the authoritative source of attachment information; it appears in the event payload immediately. GET /v1/chats/:dialogId/messagesdoes 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:
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
personalandsupervisor, setup in your Bitrix24 account is required (see Register a bot). - 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_DENIEDeven with the correct scope. - The
diskscope on the key is necessary but not sufficient — it opens the/v1/files/:idendpoint, 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.
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. 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 fields. Find the original message by message.id.
ONIMBOTV2MESSAGEDELETE
Message deleted. Instead of a message object it contains messageId (a number).
{
"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.
{
"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.
{
"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 with messageId from message.id.
ONIMBOTV2REACTIONCHANGE
A reaction was added or removed on a bot message.
{
"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) |
action |
"add" — added, "delete" — removed |
ONIMBOTV2DELETE
Bot removed from the Bitrix24 account. Contains only the bot object — without chat and user.
{
"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.
{
"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.