
# Bot platform

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

**Scope:** `imbot` | **Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** `X-Api-Key`

[Which key to use](#which-key-to-use) | [Bot ownership](#bot-ownership) | [Where the bot runs](#where-the-bot-runs) | [Quick start](#quick-start) | [Echo bot (example)](#full-example-echo-bot) | [Endpoint reference](#endpoint-reference) | [Error codes](#error-codes) | [See also](#see-also)

On the international `.com` platform, an active Vibe+ trial unlocks agents and managed bots. A regular paid Bitrix24 plan without that trial does not qualify. Access is retained on a paid Vibe+ plan. Managed bots also require the platform's bot feature to be enabled.

The trial permits one VM on the smallest supported plan, shared across agents and managed bots. It does not include Galaxy or paid AI usage beyond the included quota. The trial does not change wallet top-up availability. A managed bot provides the server, API key and Bitrix24 bot registration; it is not a ready-made AI chatbot.

When the trial ends, create and start requests are rejected with `403 VIBE_DEMO_EXPIRED`. The agent or bot is retained, and its demo VM is frozen by the scheduled access check; the physical stop is asynchronous. Viewing, stopping and deleting a retained agent or bot stay available after expiry, so its frozen VM can still be shut down and removed. Buying a Vibe+ plan restores full access but does not start the VM automatically; start it through the usual controls.

## Documentation sections

- [Bot management](/docs/bots/management) — registration, update, deletion
- [Events](/docs/bots/events) — polling incoming messages and commands
- [Messages](/docs/bots/messages) — sending, editing, deleting, formatting
- [Chats](/docs/bots/chats) — creating chats, managing participants and managers
- [Commands](/docs/bots/commands) — commands that a user calls with `/` in a chat
- [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
- [Bot access recovery](/docs/bots/ownership-recovery) — the bot is missing from the list while its code is still in use

## Message formatting

Reference for formatting message text when sending and editing messages:

- [Text formatting (BB-codes)](/docs/bots/messages/formatting)
- [Keyboard](/docs/bots/messages/keyboard)
- [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: your own server, a background process, a personal script | Personal API key `vibe_api_…` | `X-Api-Key: vibe_api_…` |
| Bot inside an OAuth application 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 your Vibecode account 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 application 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 the Bearer token, bot endpoints return `401 TOKEN_MISSING`. Suitable for bots that act on behalf of different users of the Bitrix24 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 the bot — 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 made with the application's key will fail. Register the bot and work with it using the same key.

[Transfer bot ownership](/docs/bots/management/transfer). An app-owned OAuth bot can transfer only to an eligible key of the same OAuth application (`client_id`). Restore access to that matching application and key; a recreated application with another `client_id` cannot take ownership: `400 TARGET_KEY_INVALID` with `error.reason=application_mismatch`. If the matching application cannot be restored, contact support. Unmarked legacy/token-based and personal-webhook bots retain their existing transfer rules and can use an eligible key of the same Bitrix24 account. Transfer to a compatible key preserves `botId`, chats and history.

Because of that same binding, a bot registered with a different key never appears in `GET /v1/bots`, even though its `code` is still in use in the Bitrix24 account. The step-by-step recovery procedure is in [Bot access recovery](/docs/bots/ownership-recovery).

---

## Where the bot runs

Event polling is a continuously running process, so the bot needs a machine that stays online. You can host the bot on a Black Hole server: create one through [`POST /v1/infra/servers`](/docs/infra/servers/create) and deploy the bot code onto it through the [Deploy API](/docs/infra/deploy).

**Disable automatic sleep before you start the bot.** A new virtual machine is created with a 60-minute idle timeout, and idleness is measured by inbound requests **to** the application — requests to its HTTPS subdomain. The bot sends outbound polling requests, so those requests do not reset the idle timer: after an hour the machine stops together with the bot process, and the bot stops receiving messages. To disable automatic sleep, call [`PATCH /v1/infra/servers/:id/sleep`](/docs/infra/lifecycle/sleep) with `sleepAfterMinutes: null`. The current timeout and the machine status are returned by [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) — on a stopped machine `status` is `sleeping`.

The same timer also stops the server of a bot running in `webhook` mode: a sleeping machine will not receive the event, and there are no delivery retries. Both cases are covered in [The bot was working and went silent](/docs/bots/troubleshooting#the-bot-was-working-and-went-silent).

---

## 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": {
    "botId": 42,
    "bot": {
      "id": 42,
      "code": "my_helper_bot",
      "type": "bot",
      "eventMode": "fetch"
    },
    "users": [
      { "id": 42, "name": "Assistant", "workPosition": "Chat bot", "active": true, "bot": true }
    ]
  }
}
```

> Take the bot ID from `data.botId` — the platform stores `botToken` internally and does not return it in the response. The bot name is returned in the `data.users` array, not in the `data.bot` object.

### 2. Receive events by polling

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

The request does not wait for new events and responds immediately — with an empty `events` array when the queue is empty. Repeat it in a loop with a 2–5 second pause.

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": {
          "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=36"
```

### 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

Polling retries `429 RATE_LIMITED` / `CALLER_RATE_LIMITED` and transient `409` / `503` errors only after `Retry-After`. On `500 INTERNAL_ERROR`, it preserves the position and retries with an increasing delay from 5 to 60 seconds; a successful response resets the delay. Terminal `409` stops the loop: for `BOT_ID_REPLACED`, update the ID using `error.details.botId` before restarting; for `BOT_UNAVAILABLE`, contact support. Other terminal refusals require fixing their cause. See [Polling errors](/docs/bots/events/polling).

```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.botId

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: { en: 'Check whether the bot is alive', de: 'Prüfen, ob der Bot aktiv ist' }
  })
})

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: { en: 'Show the command list', de: 'Befehlsliste anzeigen' }
  })
})

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

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 payload = await res.json()
      if (!res.ok) {
        if (res.status === 500 && payload.error?.code === 'INTERNAL_ERROR') {
          await new Promise(r => setTimeout(r, backoffMs))
          backoffMs = Math.min(backoffMs * 2, 60000)
          continue
        }
        const retryable =
          (res.status === 429 && ['RATE_LIMITED', 'CALLER_RATE_LIMITED'].includes(payload.error?.code)) ||
          (res.status === 409 && ['BOT_EVENTS_BUSY', 'BOT_RECOVERY_IN_PROGRESS'].includes(payload.error?.code)) ||
          (res.status === 503 && payload.error?.code === 'BOT_EVENTS_UNAVAILABLE')
        const retryAfter = Number(res.headers.get('Retry-After'))
        if (retryable && Number.isFinite(retryAfter) && retryAfter > 0) {
          await new Promise(r => setTimeout(r, retryAfter * 1000))
          continue
        }
        console.error('Polling stopped:', payload.error)
        return
      }
      const data = payload.data
      backoffMs = 5000

      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. Send me a message and I will repeat it.\n\nCommands:\n[SEND=/ping]Ping[/SEND] | [SEND=/help]Help[/SEND]'
        }
      })
    })
  }
}

poll()
```

---

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

Scenario: in the morning the bot sends each employee a **private message**, collects the answers, and posts a summary to a **group chat**. Two different `dialogId` values matter here (see [Send 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 `data.chat.dialogId`, which is `chatXXX` for a group chat. See [Events](/docs/bots/events).

### Where to get participant IDs

`GET /v1/bots/:botId/chats/:dialogId/users` returns the chat participant list; alternatively, fetch 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 send the morning questions and publish the report on your own schedule (`crontab`, `node-cron`, etc.). Keep event polling running continuously — see [echo bot](#full-example-echo-bot) above. Polling on its own does not keep the machine online, so automatic sleep must be disabled on it — [Where the bot runs](#where-the-bot-runs).

---

## 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 display issue — those bytes are lost on the client side, before the HTTP request is even sent.

**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 **without a UTF-8 BOM** — Windows PowerShell 5.1 reads a BOM-less file in the system ANSI code page, so every non-ASCII character in the script is garbled. A file with a BOM is read as UTF-8 correctly.
- 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 body encoding to the server.

**Node.js and Python work out of the box** — `fetch` with JSON in the request body and the `requests` library encode the body to UTF-8 themselves, so no extra steps are required. The problem is specific to PowerShell.

---

## Endpoint reference

This table is the list of bot capabilities on Vibecode — use it to check whether a capability exists. The Bitrix24 REST method list is not suitable for that check: it is incomplete with regard to Bot API v2, and a check against it reports a working capability as missing. See [Bot API v2 methods are missing from the Bitrix24 REST method list](/docs/bots/troubleshooting#bot-api-v2-methods-are-missing-from-the-bitrix24-rest-method-list).

All 38 bot platform endpoints:

| Method | Path | Bitrix24 method | Description |
|-------|------|---------------|---------|
| POST | [/v1/bots](/docs/bots/management/create) | imbot.v2.Bot.register | Register a bot |
| POST | [/v1/bots/registrations/:code/recover](/docs/bots/management/registrations-recover) | — | Recover a bot registration |
| 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 |
| POST | [/v1/bots/:botId/transfer](/docs/bots/management/transfer) | — | Transfer bot ownership to another key |
| 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 as 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 |

A bot hands its session to a person through [bot session actions](/docs/bots/openlines). The bot leaving the chat does not transfer the session. Both `imbot` and `imopenlines` scopes are required.

---

## 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. To regain control, see [Bot access recovery](/docs/bots/ownership-recovery) |
| `BOT_ALREADY_EXISTS` | 409 | A bot with that `code` is already registered. The response contains the existing bot's `data.botId` — the same field path as in a successful 201. If that bot is missing from `GET /v1/bots`, see [Bot access recovery](/docs/bots/ownership-recovery) |
| `BOT_UNAVAILABLE` | 409 | The bot policy or lifecycle state requires repair. A Bitrix24 call may already have occurred and its outcome may be uncertain. No `Retry-After` is returned. Stop automatic retries and contact support with the request time; check the key and owning application. |
| `REGISTRATION_FAILED` | 502 | Bitrix24 did not return a bot ID during registration |

### System errors

| Code | HTTP | Description |
|-----|------|---------|
| `MISSING_API_KEY` | 401 | Missing `X-Api-Key` header |
| `INVALID_API_KEY` | 401 | The key is not recognized — no such key exists on the platform |
| `KEY_INACTIVE` | 401 | The key is revoked or blocked by the platform |
| `RATE_LIMITED` | 429 | Request limit exceeded |
| `BITRIX_ERROR` | 422 | Error in the Bitrix24 API response |
| `BITRIX_UNAVAILABLE` | 502 | The Bitrix24 portal is unavailable |
| `INTERNAL_ERROR` | 500 | Internal server error. When polling events, retry with an increasing 5–60 second delay without `Retry-After`; reset the delay after success |

Full list of common API errors — [Errors](/docs/errors).

## See also

- [Chats](/docs/chats)
- [Open Channels](/docs/openlines)
- [Keys and authorization](/docs/keys-auth)
- [Limits and optimization](/docs/optimization)
- [Errors](/docs/errors)
