#Bot platform

Build Bitrix24 chat bots: register a bot, receive user messages and commands, reply with text, buttons, and attachments, manage group chats and participants. No webhooks or public server required — the bot polls for events and replies over regular HTTP requests.

Scope: imbot | Base URL: https://vibecode.bitrix24.tech/v1 | Auth: X-Api-Key

Which key to use | Quick start | Echo bot (example) | Troubleshooting | Error codes | Endpoint reference

#Documentation sections

  • Bot management — registration, update, deletion
  • Events — polling incoming messages and commands
  • Messages — send, edit, delete, formatting
  • Chats — creating chats, managing participants and managers
  • Commands — bot slash commands
  • Interface — reactions, typing indicator, input field
  • Files — upload and download
  • Troubleshooting — what to do if the bot does not receive events or returns errors

#Message formatting

Reference for formatting message text when sending and editing:


#Which key to use

The bot platform works with two key types. The choice depends on whose identity the bot uses to call Bitrix24.

Scenario Key Request headers
Bot for your own portal: own server, background process, personal script Personal API key vibe_api_… X-Api-Key: vibe_api_…
Bot inside an OAuth app published in the VibeCode catalog and installed on user portals Authorization key vibe_app_… X-Api-Key: vibe_app_… + Authorization: Bearer <session_token>

Personal key vibe_api_…. Created in the VibeCode dashboard and bound to a single Bitrix24 portal. Every request runs as the key owner; no additional session token is required. Suitable for one-off bots that are not published as an app.

Authorization key vibe_app_…. Bound to an OAuth app from the catalog. Each request runs as the user who installed the app on their portal and completed OAuth authorization — so the Authorization: Bearer <session_token> header is mandatory. Without Bearer, bot endpoints return 401 TOKEN_MISSING. Suitable for distributable bots that run on different portals under different users.

A detailed description of key types, formats, and obtaining a session_tokenKeys and authorization.


#Quick start

#1. Register a bot

Terminal
curl -X POST https://vibecode.bitrix24.tech/v1/bots \
  -H "X-Api-Key: $VIBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "my_helper_bot",
    "name": "Assistant",
    "type": "bot",
    "eventMode": "fetch"
  }'

Response:

JSON
{
  "success": true,
  "data": {
    "bot": {
      "id": 42,
      "code": "my_helper_bot",
      "name": "Assistant",
      "type": "bot",
      "eventMode": "fetch"
    }
  }
}

Take the bot ID from data.bot.id — the platform stores botToken itself and does not return it in the response.

#2. Receive events (long-polling)

Terminal
curl -H "X-Api-Key: $VIBE_KEY" \
  "https://vibecode.bitrix24.tech/v1/bots/42/events"

The response contains nextOffset — pass it as offset in the next request:

JSON
{
  "success": true,
  "data": {
    "events": [
      {
        "eventId": 35,
        "type": "ONIMBOTV2MESSAGEADD",
        "date": "2026-04-13T10:05:00+03:00",
        "data": {
          "dialogId": "12",
          "bot": { "id": 42, "code": "my_helper_bot", "type": "bot" },
          "message": {
            "id": 1501,
            "chatId": 87,
            "authorId": 1,
            "date": "2026-04-13T10:05:00+03:00",
            "text": "Hi, bot!"
          },
          "chat": { "id": 87, "dialogId": "12", "type": "private" },
          "user": { "id": 1, "name": "John Smith", "firstName": "John", "lastName": "Smith" }
        }
      }
    ],
    "nextOffset": 36,
    "hasMore": false,
    "storedOffset": 35,
    "persisted": true
  }
}

Next request:

Terminal
curl -H "X-Api-Key: $VIBE_KEY" \
  "https://vibecode.bitrix24.tech/v1/bots/42/events?offset=42"

#3. Send a reply

Terminal
curl -X POST https://vibecode.bitrix24.tech/v1/bots/42/messages \
  -H "X-Api-Key: $VIBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dialogId": "12",
    "fields": { "message": "Hi! How can I help?" }
  }'

Response:

JSON
{
  "success": true,
  "data": {
    "id": 1502,
    "uuidMap": []
  }
}

#Full example: Echo bot

javascript
const VIBE_KEY = process.env.VIBE_KEY
const BASE = 'https://vibecode.bitrix24.tech/v1'

// ── 1. Register the bot ─────────────────────────────────────────
const regRes = await fetch(`${BASE}/bots`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code: 'echo_bot',
    name: 'Echo Bot',
    type: 'bot',
    color: 'AQUA',
    eventMode: 'fetch',
    workPosition: 'Repeats your messages'
  })
})
const { data } = await regRes.json()
const BOT_ID = data.bot.id

console.log(`Bot registered with ID: ${BOT_ID}`)

// ── 2. Register slash commands ─────────────────────────────────
await fetch(`${BASE}/bots/${BOT_ID}/commands`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    command: 'ping',
    title: 'Ping',
    description: 'Check whether the bot is alive'
  })
})

await fetch(`${BASE}/bots/${BOT_ID}/commands`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    command: 'help',
    title: 'Help',
    description: 'Show the command list'
  })
})

// ── 3. Event polling loop ───────────────────────────────────────
let offset = undefined

async function poll() {
  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': VIBE_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)
    }

    await new Promise(r => setTimeout(r, 3000))
  }
}

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

  // ── Handle messages ────────────────────────────────────────
  if (event.type === 'ONIMBOTV2MESSAGEADD') {
    const dialogId = data.chat.dialogId
    const text = data.message?.text || ''
    const userName = data.user?.firstName || 'friend'

    // Skip system messages and our own
    if (data.message?.isSystem) return
    if (data.message?.authorId === BOT_ID) return

    // Show "thinking..."
    await fetch(`${BASE}/bots/${BOT_ID}/typing`, {
      method: 'POST',
      headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        dialogId,
        statusMessageCode: 'IMBOT_AGENT_ACTION_THINKING'
      })
    })

    // Add a reaction to the message
    await fetch(`${BASE}/bots/${BOT_ID}/messages/${data.message.id}/reactions`, {
      method: 'POST',
      headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ reaction: 'like' })
    })

    // Send an echo reply with a keyboard
    await fetch(`${BASE}/bots/${BOT_ID}/messages`, {
      method: 'POST',
      headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        dialogId,
        fields: {
          message: `${userName}, you wrote: [I]${text}[/I]`,
          keyboard: [
            {
              TEXT: 'Repeat',
              BG_COLOR_TOKEN: 'primary',
              ACTION: 'SEND',
              ACTION_VALUE: text || 'ping',
              BLOCK: 'Y'
            },
            {
              TEXT: 'Help',
              BG_COLOR_TOKEN: 'secondary',
              ACTION: 'SEND',
              ACTION_VALUE: '/help',
              DISPLAY: 'LINE'
            }
          ]
        }
      })
    })
  }

  // ── Handle commands ────────────────────────────────────────
  if (event.type === 'ONIMBOTV2COMMANDADD') {
    const dialogId = data.chat.dialogId
    const command = data.command

    if (command.command === 'ping') {
      await fetch(`${BASE}/bots/${BOT_ID}/commands/${command.id}/answer`, {
        method: 'POST',
        headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messageId: data.message.id,
          message: '[B]Pong![/B] The bot is working fine.'
        })
      })
    }

    if (command.command === 'help') {
      await fetch(`${BASE}/bots/${BOT_ID}/commands/${command.id}/answer`, {
        method: 'POST',
        headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messageId: data.message.id,
          message: '[B]Echo Bot — Commands[/B]\n\n/ping — check whether the bot is alive\n/help — show this list\n\nJust message me — I will repeat your message.'
        })
      })
    }
  }

  // ── Handle reactions ───────────────────────────────────────
  if (event.type === 'ONIMBOTV2REACTIONCHANGE') {
    const dialogId = data.chat.dialogId
    const userName = data.user?.firstName || 'Someone'

    if (data.action === 'add') {
      await fetch(`${BASE}/bots/${BOT_ID}/messages`, {
        method: 'POST',
        headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          dialogId,
          fields: {
            message: `${userName} added a reaction: ${data.reaction}`,
            system: true
          }
        })
      })
    }
  }

  // ── Join chat ──────────────────────────────────────────────
  if (event.type === 'ONIMBOTV2JOINCHAT') {
    await fetch(`${BASE}/bots/${BOT_ID}/messages`, {
      method: 'POST',
      headers: { 'X-Api-Key': VIBE_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        dialogId: data.chat.dialogId,
        fields: {
          message: 'Hi! I am Echo Bot. Write me something and I will repeat it.\n\nCommands:\n[SEND=/ping]Ping[/SEND] | [SEND=/help]Help[/SEND]'
        }
      })
    })
  }
}

poll()

#Full example: Standup bot (private poll → report to a group)

A common scenario that lacks an explicit example: in the morning the bot messages each employee privately, collects the answers, and posts a summary to a group chat. Two different dialogId values matter here (see Send a message):

  • private messagedialogId equals the numeric user ID ("42");
  • group chatdialogId has the form chatXXX.

#Where to get the group chat `dialogId`

  • Create a chat: POST /v1/bots/:botId/chats (Chat.add) — the response carries data.chat.dialogId = chatXXX. See Create a chat.
  • Or take it from an incoming event: every message event from GET /v1/bots/:botId/events carries a dialogId (for a group chat it is chatXXX). See Events.

#Where to get participant IDs

GET /v1/bots/:botId/chats/:dialogId/users returns the chat participant list, or take employees from GET /v1/users (scope user). Each user's numeric ID is their private dialogId.

#Steps

javascript
const BASE = 'https://vibecode.bitrix24.tech/v1'
const headers = { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }

// 1. Morning: a private question to each participant (dialogId = numeric userId)
async function askTeam(botId, userIds) {
  for (const userId of userIds) {
    await fetch(`${BASE}/bots/${botId}/messages`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        dialogId: String(userId),
        fields: { message: 'Good morning! What did you do yesterday, what are you planning today, what is blocking you?' },
      }),
    })
  }
}

// 2. During the day: collect answers via polling.
//    Event fields are nested under ev.data (see "Events"): dialogId, message.text, user.id.
//    In a private dialog ev.data.dialogId equals the sender's numeric ID.
const answers = {}
async function collect(botId, expectedUserIds) {
  const res = await fetch(`${BASE}/bots/${botId}/events`, { headers })
  const { data } = await res.json()
  for (const ev of data.events ?? []) {
    if (ev.type !== 'ONIMBOTV2MESSAGEADD') continue
    const fromUser = String(ev.data.user.id)
    // Private dialog: dialogId equals the sender ID
    if (ev.data.dialogId === fromUser && expectedUserIds.includes(fromUser)) {
      answers[fromUser] = ev.data.message.text
    }
  }
}

// 3. Publish the summary to the group chat (dialogId = chatXXX)
async function publishReport(botId, groupDialogId, userIds) {
  const lines = userIds.map(id => `[b]${id}[/b]: ${answers[String(id)] ?? '— no answer'}`)
  await fetch(`${BASE}/bots/${botId}/messages`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      dialogId: groupDialogId, // 'chat123'
      fields: { message: `[b]Standup for today[/b]\n${lines.join('\n')}` },
    }),
  })
}

Scheduling lives on your server. The platform does not schedule runs for you: the bot lives on your Black Hole server, so trigger the morning poll and the report publish via your own cron (crontab, node-cron, etc.). Keep event polling running continuously — see Echo bot above.


#Windows / PowerShell and UTF-8

When sending requests to the bot API from Windows PowerShell, Cyrillic in the bot name or in the message body can turn into question marks (?). This is not a server-side rendering issue — the Cyrillic bytes are lost before the HTTP request is even sent, on the client side.

Cause: by default PowerShell uses the windows-1251 encoding (or iso-8859-1 — depending on the locale) for strings passed to the -Body parameter of Invoke-WebRequest / Invoke-RestMethod. The source string loses its Cyrillic before the HTTP client assembles the request. A Content-Type: charset=utf-8 header does not help here — by the time it applies, the original bytes are already lost during the client-side re-encoding.

Solution: set UTF-8 explicitly and pass the body as a byte array.

powershell
# 1. Enable UTF-8 for PowerShell (done once per session)
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 2. Build the JSON and convert it to a UTF-8 byte array
$body = @{
  code = 'my_bot'
  name = 'My bot'
  color = 'AZURE'
} | ConvertTo-Json -Compress

$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)

# 3. Pass the byte array to -Body (not a string!) and set the encoding explicitly in Content-Type
Invoke-WebRequest `
  -Uri 'https://vibecode.bitrix24.tech/v1/bots' `
  -Method POST `
  -Headers @{
    'X-Api-Key'    = $env:VIBE_KEY
    'Content-Type' = 'application/json; charset=utf-8'
  } `
  -Body $bytes

Common mistakes:

  • ❌ Saving the .ps1 file with a UTF-8 BOM — older PowerShell versions may fail to parse the script itself.
  • ❌ Passing a string to -Body (-Body $body) instead of a byte array (-Body $bytes) — the string is re-encoded again through the system encoding.
  • ❌ Relying solely on Content-Type: application/json; charset=utf-8 without UTF8.GetBytes — this header does not restore bytes lost during client-side re-encoding; it only declares the claimed body encoding to the server.

Node.js and Python work by defaultfetch with JSON in the request body and the requests library encode the body to UTF-8 themselves, no extra steps required. The problem is specific to PowerShell.


#Error codes

#Bot platform errors

Code HTTP Description
SCOPE_DENIED 403 API key lacks the imbot scope
TOKEN_MISSING 401 API key has no configured tokens
CODE_REQUIRED 400 The code parameter was not provided during registration
NAME_REQUIRED 400 The name parameter was not provided during registration
INVALID_BOT_ID 400 botId must be a number
BOT_NOT_FOUND 404 No bot with that ID found — register it via POST /v1/bots
BOT_ACCESS_DENIED 403 The bot belongs to a different API key
BOT_ALREADY_EXISTS 409 A bot with that code is already registered. The response contains the existing bot's data.botId (a flat shape on this path, unlike the nested data.bot.id in a successful 201)
REGISTRATION_FAILED 502 Bitrix24 did not return a bot ID during registration

#System errors

Code HTTP Description
AUTH_REQUIRED 401 Missing X-Api-Key header
INVALID_API_KEY 401 Invalid API key
KEY_REVOKED 403 API key revoked
RATE_LIMIT 429 Request limit exceeded
BITRIX_ERROR 502 Error in the Bitrix24 API response
BITRIX_UNAVAILABLE 502 Bitrix24 portal unavailable
INTERNAL_ERROR 500 Internal server error

#Endpoint reference

All 36 bot platform endpoints:

Method Path Bitrix24 method Description
POST /v1/bots imbot.v2.Bot.register Register a bot
GET /v1/bots List bots
GET /v1/bots/:botId imbot.v2.Bot.get Bot data
PATCH /v1/bots/:botId imbot.v2.Bot.update Update a bot
DELETE /v1/bots/:botId imbot.v2.Bot.unregister Delete a bot
POST /v1/bots/:botId/reauth Re-authorize a bot
POST /v1/bots/:botId/resubscribe imbot.v2.Bot.update Re-subscribe to events
GET /v1/bots/revision imbot.v2.Revision.get Bot platform revision
GET /v1/bots/:botId/events imbot.v2.Event.get Get events (polling)
POST /v1/bots/:botId/messages imbot.v2.Chat.Message.send Send a message
PATCH /v1/bots/:botId/messages/:messageId imbot.v2.Chat.Message.update Update a message
DELETE /v1/bots/:botId/messages/:messageId imbot.v2.Chat.Message.delete Delete a message
POST /v1/bots/:botId/chats/:dialogId/read imbot.v2.Chat.Message.read Mark messages read
GET /v1/bots/:botId/messages/:messageId imbot.v2.Chat.Message.get Get a message
GET /v1/bots/:botId/messages/:messageId/context imbot.v2.Chat.Message.getContext Message context
POST /v1/bots/:botId/messages/:messageId/reactions imbot.v2.Chat.Message.Reaction.add Add a reaction
DELETE /v1/bots/:botId/messages/:messageId/reactions imbot.v2.Chat.Message.Reaction.delete Remove a reaction
POST /v1/bots/:botId/typing imbot.v2.Chat.InputAction.notify Typing indicator
POST /v1/bots/:botId/text-field imbot.v2.Chat.TextField.enabled Manage the input field
POST /v1/bots/:botId/chats imbot.v2.Chat.add Create a chat
GET /v1/bots/:botId/chats/:dialogId imbot.v2.Chat.get Chat information
PATCH /v1/bots/:botId/chats/:dialogId imbot.v2.Chat.update Update a chat
POST /v1/bots/:botId/chats/:dialogId/leave imbot.v2.Chat.leave Leave a chat
POST /v1/bots/:botId/chats/:dialogId/owner imbot.v2.Chat.setOwner Set the owner
POST /v1/bots/:botId/chats/:dialogId/users imbot.v2.Chat.User.add Add participants
DELETE /v1/bots/:botId/chats/:dialogId/users imbot.v2.Chat.User.delete Remove a participant
GET /v1/bots/:botId/chats/:dialogId/users imbot.v2.Chat.User.list List participants
POST /v1/bots/:botId/chats/:dialogId/managers imbot.v2.Chat.Manager.add Add managers
DELETE /v1/bots/:botId/chats/:dialogId/managers imbot.v2.Chat.Manager.delete Remove managers
POST /v1/bots/:botId/commands imbot.v2.Command.register Register a command
GET /v1/bots/:botId/commands imbot.v2.Command.list List commands
PATCH /v1/bots/:botId/commands/:commandId imbot.v2.Command.update Update a command
DELETE /v1/bots/:botId/commands/:commandId imbot.v2.Command.unregister Delete a command
POST /v1/bots/:botId/commands/:commandId/answer imbot.v2.Command.answer Answer a command
POST /v1/bots/:botId/files imbot.v2.File.upload Upload a file
GET /v1/bots/:botId/files/:fileId imbot.v2.File.download Download a file