
# 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.com/v1` | **Auth:** `X-Api-Key`

[Which key to use](#which-key-to-use) | [Bot ownership](#bot-ownership) | [Quick start](#quick-start) | [Echo bot (example)](#full-example-echo-bot) | [Troubleshooting](/docs/bots/troubleshooting) | [Error codes](#error-codes) | [Endpoint reference](#endpoint-reference)

## Documentation sections

- [Bot management](/docs/bots/management) — registration, update, deletion
- [Events](/docs/bots/events) — polling incoming messages and commands
- [Messages](/docs/bots/messages) — send, edit, delete, formatting
- [Chats](/docs/bots/chats) — creating chats, managing participants and managers
- [Commands](/docs/bots/commands) — bot slash commands
- [Interface](/docs/bots/ui) — reactions, typing indicator, input field
- [Files](/docs/bots/files) — upload and download
- [Troubleshooting](/docs/bots/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:

- [Text formatting (BB-codes)](/docs/bots/messages/formatting)
- [Keyboard](/docs/bots/messages/keyboard)
- [Attachments (ATTACH blocks)](/docs/bots/messages/attach)

---

## 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 Bitrix24 account: 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 a Bitrix24 account | 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 account. 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 Bitrix24 account and completed OAuth authorization — so the `Authorization: Bearer <session_token>` header is mandatory. Without Bearer, bot endpoints return `401 TOKEN_MISSING`. Suitable for bots that act on behalf of different users of the account where the app is registered.

For a detailed description of key types, formats, and how to obtain a `session_token`, see [Keys and authorization](/docs/keys-auth).

---

## Bot ownership

A bot is bound to the API key it was registered with. All bot operations — polling events, sending messages, updating — run with that same key. A request from a different key returns `403 BOT_ACCESS_DENIED`.

This matters during deployment: if the application is deployed with one key while the bot was created with another, calls to bot endpoints with that key will fail. Register the bot and work with it using one and the same key.

To move a bot to another key — for example, when the original key was revoked after the app was recreated — use [`POST /v1/bots/:botId/transfer`](/docs/bots/management/transfer). The target key must be active and belong to the same user, or the transfer must be performed by an account admin. Only the key binding changes; `botId` and chat history are preserved.

---

## Quick start

### 1. Register a bot

```bash
curl -X POST https://vibecode.bitrix24.com/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` on its side and does not return it in the response.

### 2. Receive events (long-polling)

```bash
curl -H "X-Api-Key: $VIBE_KEY" \
  "https://vibecode.bitrix24.com/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+00: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+00: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:

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

### 3. Send a reply

```bash
curl -X POST https://vibecode.bitrix24.com/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.com/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({
          dialogId,
          messageId: data.message.id,
          fields: { 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({
          dialogId,
          messageId: data.message.id,
          fields: { 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](/docs/bots/messages/send)):

- **private message** — `dialogId` equals the numeric user ID (`"42"`);
- **group chat** — `dialogId` 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](/docs/bots/chats/create).
- 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](/docs/bots/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.com/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"): chat.dialogId, message.text, user.id.
//    In a private dialog ev.data.chat.dialogId equals the sender's numeric ID — the
//    same path the echo bot uses above.
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: take dialogId from ev.data.chat.dialogId
    if (ev.data.chat.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 publish the report from your own cron (`crontab`, `node-cron`, etc.). Keep event polling running continuously — see [Echo bot](#full-example-echo-bot) above.

---

## Windows / PowerShell and UTF-8

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

**Cause:** by default PowerShell re-encodes strings passed to the `-Body` parameter of `Invoke-WebRequest` / `Invoke-RestMethod` into the system's legacy (non-UTF-8) code page — for example `windows-1252` on Western installs — and any character outside that page is lost 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:** pass the request body as a UTF-8 byte array.

```powershell
# 1. Console output encoding — does not affect how the body is encoded
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 2. Build the JSON and convert it to a UTF-8 byte array
$body = @{
  code = 'my_bot'
  name = 'Café 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.com/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 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 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 | The Bitrix24 portal is unavailable |
| `INTERNAL_ERROR` | 500 | Internal server error |

---

## Endpoint reference

All 36 bot platform endpoints:

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