#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_token — Keys and authorization.
#Quick start
#1. Register a bot
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:
{
"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 storesbotTokenitself and does not return it in the response.
#2. Receive events (long-polling)
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:
{
"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:
curl -H "X-Api-Key: $VIBE_KEY" \
"https://vibecode.bitrix24.tech/v1/bots/42/events?offset=42"
#3. Send a reply
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:
{
"success": true,
"data": {
"id": 1502,
"uuidMap": []
}
}
#Full example: Echo bot
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 message —
dialogIdequals the numeric user ID ("42"); - group chat —
dialogIdhas the formchatXXX.
#Where to get the group chat `dialogId`
- Create a chat:
POST /v1/bots/:botId/chats(Chat.add) — the response carriesdata.chat.dialogId=chatXXX. See Create a chat. - Or take it from an incoming event: every message event from
GET /v1/bots/:botId/eventscarries adialogId(for a group chat it ischatXXX). 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
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.
# 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
.ps1file 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-8withoutUTF8.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 default — fetch 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 |