How to Create a Chatbot for Open Channels
Scope:
imbot,imopenlinesWho can execute the methods: to complete the whole scenario, you need the strictest of the listed permissions — the user of the application or webhook that the chatbot is registered through
- imbot.v2.Bot.register — authenticated user
- imopenlines.bot.session.message.send — any user
- imopenlines.bot.session.operator — any user
- imopenlines.bot.session.transfer and imopenlines.bot.session.finish — application user with a registered chatbot
Choose a tool for developing with an AI agent:
- use Alaio Vibecode to build an app for Bitrix24 from a task description without knowing any programming language. The agent writes the code and deploys the app to a server, with no manual hosting setup
- use the MCP server to develop a REST API integration in your own project. The agent refers to the official REST documentation
An Open Channels chatbot receives customer requests, sends the first response, and transfers the conversation to an operator when needed. For this scenario, use the current chatbots 2.0 platform.
Verifiable result: the customer writes to an Open Channel, the bot replies with an automatic message, and on the "operator" keyword it transfers the conversation to an employee. Both the bot reply and the connected operator are visible in the channel chat.
Three objects take part in the scenario:
- an inbound webhook with the
imbotandimopenlinespermissions - a registered chatbot with Open Channels support
- an Open Channel this bot is connected to
SDKs perform outgoing REST calls only. Incoming events are received by your web server — an Express or Flask application, or a plain PHP script.
The scenario consists of four steps.
- Register a bot with Open Channels support using the imbot.v2.Bot.register method and connect it to the channel
- Receive the ONIMBOTV2MESSAGEADD event in the handler and check that the chat belongs to an Open Channel
- Reply to the customer using the imopenlines.bot.session.message.send method
- Transfer the conversation to an operator or finish the session using the imopenlines.bot.session.operator, imopenlines.bot.session.transfer, and imopenlines.bot.session.finish methods
The order is set by the platform: the chat identifier for session management appears only in the event, and the events arrive only after the bot is registered.
Prepare the Data
The examples on this page work through an inbound webhook: it does not require installing an application, and the bot is registered in Bitrix24 right away. The differences of the scenario for an application with OAuth authorization are collected in the Important Notes block.
- Create an inbound webhook with the
imbotandimopenlinespermissions - Host the event handler on a public HTTPS URL, for example
https://example.com/handler - Set up an Open Channel and connect a communication channel to it — the website live chat or a messenger
Only a Bitrix24 administrator can create a webhook and set up an Open Channel.
Prepare the values that you need to replace with your own:
|
Value |
Where to take it from |
|
|
Inbound webhook URL in the form |
|
|
Come up with a unique bot token up to 40 characters long. It is bound to the bot during the registration |
|
|
Public HTTPS address of the event handler. In the JS and Python examples the handler listens on the |
|
|
ID of the employee the bot transfers the conversation to. You can see it in the URL of the employee profile or in the response of the user.get and user.search methods — these two methods need a separate |
You do not need to substitute the chat identifier: it arrives in the data.chat.id field of the event and is passed to the CHAT_ID parameter of the session management methods.
The inbound webhook URL and the bot token are secrets. The URL grants the whole access of the webhook, and the token allows managing sessions on behalf of the bot. Retain both values in the server environment variables and do not place them in code that runs in the browser.
For the Python example, split the webhook URL into the B24_DOMAIN domain and the B24_WEBHOOK_TOKEN path in the form 1/xxxxxxxxxxxxxxxx.
Initialize the SDK and read the prepared values before the first call.
How to Use Examples in Documentation
// npm install express @bitrix24/b24jssdk
import { B24Hook, Text } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_WEBHOOK_URL)
const botToken = process.env.BOT_TOKEN
const handlerUrl = process.env.HANDLER_URL
const operatorId = Number(process.env.OPERATOR_ID)
// The SDK does not throw an exception on a REST error, so we check the isSuccess flag
async function call(method, params) {
const response = await $b24.actions.v2.call.make({
method,
params,
requestId: Text.getUuidRfc4122(),
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
return response.getData().result
}
# pip install b24pysdk flask
import os
from b24pysdk import BitrixWebhook, Client
token = BitrixWebhook(
domain=os.environ["B24_DOMAIN"],
webhook_token=os.environ["B24_WEBHOOK_TOKEN"],
)
client = Client(token)
bot_token = os.environ["BOT_TOKEN"]
handler_url = os.environ["HANDLER_URL"]
operator_id = int(os.environ["OPERATOR_ID"])
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Symfony\Component\EventDispatcher\EventDispatcher;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$b24 = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook(getenv('B24_WEBHOOK_URL'));
$botToken = getenv('BOT_TOKEN');
$handlerUrl = getenv('HANDLER_URL');
$operatorId = (int)getenv('OPERATOR_ID');
This code is needed both by the one-time registration script from step 1 and by the permanently running handler from steps 2–4. Keep it in both files or move it to a shared module.
1. Register the Bot and Connect It to the Channel
In imbot.v2.Bot.register, the bot parameters are passed in the fields object:
code— bot code, unique within the webhook or the applicationbotToken— bot token, required when authorizing through an inbound webhooktype— bot typeisSupportOpenline— Open Channels supporteventMode— event delivery mode, thewebhookvalue sends the events to the handler URL, and a separate subscription with theevent.bindmethod is not neededwebhookUrl— event handler URLproperties— bot profile: thenamename, theworkPositionjob title, and thecoloravatar color
Choose the type according to your task. For a hybrid bot that works in group chats, private conversations, and Open Channels, pass type = bot and isSupportOpenline = true. If the bot is needed only for Open Channels, pass type = openline — the channel support turns on by itself. Without it, the bot does not receive the events from the channel chat.
The registration is performed once. In the Python and PHP examples the SDKs throw an exception themselves if the method returns an error, and in JS the calls go through the call function from the preparation block — it checks the isSuccess flag.
// There is no typed wrapper for imbot.v2, so the method is called directly through the SDK core.
const result = await call('imbot.v2.Bot.register', {
fields: {
code: 'open_line_bot',
botToken: botToken,
type: 'bot',
isSupportOpenline: true,
eventMode: 'webhook',
webhookUrl: handlerUrl,
properties: {
name: 'Support line',
workPosition: 'First line',
color: 'green',
},
},
})
const botId = Number(result.bot.id)
# There is no typed wrapper for imbot.v2, so the method is called directly through the SDK core.
response = token.call_method(
"imbot.v2.Bot.register",
{
"fields": {
"code": "open_line_bot",
"botToken": bot_token,
"type": "bot",
"isSupportOpenline": True,
"eventMode": "webhook",
"webhookUrl": handler_url,
"properties": {
"name": "Support line",
"workPosition": "First line",
"color": "green",
},
}
},
)
bot_id = int(response["result"]["bot"]["id"])
// There is no typed wrapper for imbot.v2, so the method is called directly through the SDK core.
$result = $b24->core->call('imbot.v2.Bot.register', [
'fields' => [
'code' => 'open_line_bot',
'botToken' => $botToken,
'type' => 'bot',
'isSupportOpenline' => true,
'eventMode' => 'webhook',
'webhookUrl' => $handlerUrl,
'properties' => [
'name' => 'Support line',
'workPosition' => 'First line',
'color' => 'green',
],
],
])->getResponseData()->getResult();
$botId = (int)$result['bot']['id'];
In a successful response, retain result.bot.id: you need it when the webhook manages several bots. The isSupportOpenline field confirms that the bot is accepted as an Open Channels bot. The example is shortened, the full response format is on the imbot.v2.Bot.register page.
{
"result": {
"bot": {
"id": 456,
"code": "open_line_bot",
"type": "bot",
"isSupportOpenline": true,
"eventMode": "webhook"
}
}
}
The method is idempotent: a repeated call with the same fields.code returns the existing bot and does not change its data. To change the properties of a registered bot or the handler URL, use imbot.v2.Bot.update.
After the registration, connect the bot to the channel: open Contact Center > Open Channels, edit the required channel, and specify the bot in the chatbot settings block. The moment of connection is set there as well — for example, right at the first customer request.
Until the bot is connected to the channel, it does not join the session chat. The registration method still completes successfully, but the ONIMBOTV2* events from the Open Channel do not reach the handler.
2. Receive the Event and Check That the Chat Belongs to a Channel
Bitrix24 sends the bot events as a POST request to the URL from fields.webhookUrl. The request body arrives as application/x-www-form-urlencoded, and the keys have the form data[chat][entityType] and auth[application_token]. All scalar values are passed as strings, so cast the types explicitly.
The handler receives all the bot events on a single URL and routes them by the event field. The bot receives the events from all of its chats, so check the data.chat.entityType field: for Open Channels chats it equals LINES.
Take two values from the ONIMBOTV2MESSAGEADD event:
data.chat.id— chat identifier, it has to be passed to theCHAT_IDparameter of the session management methodsdata.message.text— text of the customer message, the bot chooses the reply by it
Verify the request authenticity by the auth.application_token from the top level, not by the token from data.bot.auth. For a bot registered through an inbound webhook, auth.application_token equals the custom string glued together with botToken, without a separator.
{
"event": "ONIMBOTV2MESSAGEADD",
"data": {
"bot": {"id": 456, "code": "open_line_bot"},
"message": {"id": 790, "chatId": 112, "authorId": 27, "text": "I need an operator"},
"chat": {"id": 112, "dialogId": "chat112", "type": "lines", "entityType": "LINES"},
"user": {"id": 27, "name": "Customer"}
},
"auth": {"domain": "example.bitrix24.com", "application_token": "custommy_bot_token"}
}
The example shows the event after the request body is parsed. In the request itself, the same data arrives as flat keys: data[chat][entityType]=LINES, data[chat][id]=112.
Place the sendReply and handleLinesMessage functions from steps 3 and 4 in the same file — the handler calls them by name.
// SDK initialization and the variables — from the "Prepare the Data" block
import express from 'express'
const app = express()
app.use(express.urlencoded({ extended: true }))
app.post('/handler', async (req, res) => {
const data = req.body.data || {}
const auth = req.body.auth || {}
if (auth.application_token !== `custom${botToken}`) {
return res.sendStatus(403)
}
if (req.body.event === 'ONIMBOTV2MESSAGEADD' && data.chat?.entityType === 'LINES') {
const chatId = Number(data.chat.id)
const text = String(data.message?.text ?? '').trim().toLowerCase()
try {
await handleLinesMessage(chatId, text)
} catch (error) {
console.error(error)
}
}
// The platform expects a 200 response, repeated delivery of an event is not guaranteed
res.sendStatus(200)
})
app.listen(3000)
# SDK initialization and the variables — from the "Prepare the Data" block
import re
from flask import Flask, request
app = Flask(__name__)
def unflatten(form) -> dict:
"""Assembles flat keys of the form data[chat][entityType] into a nested dictionary"""
result = {}
for key, value in form.items():
path = re.findall(r"[^\[\]]+", key)
node = result
for part in path[:-1]:
node = node.setdefault(part, {})
node[path[-1]] = value
return result
@app.post("/handler")
def handler():
payload = unflatten(request.form)
data = payload.get("data", {})
auth = payload.get("auth", {})
if auth.get("application_token") != f"custom{bot_token}":
return "", 403
chat = data.get("chat", {})
if payload.get("event") == "ONIMBOTV2MESSAGEADD" and chat.get("entityType") == "LINES":
chat_id = int(chat["id"])
text = (data.get("message", {}).get("text") or "").strip().lower()
try:
handle_lines_message(chat_id, text)
except Exception as error:
app.logger.error("%s", error)
# The platform expects a 200 response, repeated delivery of an event is not guaranteed
return "", 200
if __name__ == "__main__":
app.run(port=3000)
// Continuation of handler.php: SDK initialization and the variables — from the "Prepare the Data" block
$event = (string)($_POST['event'] ?? '');
$data = (array)($_POST['data'] ?? []);
$auth = (array)($_POST['auth'] ?? []);
if (($auth['application_token'] ?? '') !== 'custom' . $botToken) {
http_response_code(403);
exit;
}
if ($event === 'ONIMBOTV2MESSAGEADD' && ($data['chat']['entityType'] ?? '') === 'LINES') {
$chatId = (int)($data['chat']['id'] ?? 0);
$text = mb_strtolower(trim((string)($data['message']['text'] ?? '')));
try {
handleLinesMessage($chatId, $text);
} catch (Throwable $exception) {
error_log($exception->getMessage());
}
}
// The platform expects a 200 response, repeated delivery of an event is not guaranteed
http_response_code(200);
3. Reply to the Customer
A message on behalf of the bot in the current channel session is sent by the imopenlines.bot.session.message.send method. Parameters:
CHAT_ID— chat identifier, thedata.chat.idvalue from the eventNAME— reply mode:DEFAULTsends the text fromMESSAGE,WELCOMEsends the greeting from the Open Channel settings and ignoresMESSAGEMESSAGE— reply text for theDEFAULTmode. With an empty text, no message is added to the chat
The method works with the current channel session, it does not need CLIENT_ID. Wrap the call in a separate function — it comes in handy in step 4.
async function sendReply(chatId, message) {
await call('imopenlines.bot.session.message.send', {
CHAT_ID: chatId,
NAME: 'DEFAULT',
MESSAGE: message,
})
}
def send_reply(chat_id: int, message: str) -> None:
client.imopenlines.bot.session.message.send(
chat_id=chat_id,
message=message,
name="DEFAULT",
).response
function sendReply(int $chatId, string $message): void
{
global $b24;
$b24->core->call('imopenlines.bot.session.message.send', [
'CHAT_ID' => $chatId,
'NAME' => 'DEFAULT',
'MESSAGE' => $message,
]);
}
The true response confirms that the call is executed. The method does not return a confirmation that the message appeared in the chat, so check the result in the channel chat.
{
"result": true
}
4. Transfer the Conversation to an Operator or Finish the Session
With the imopenlines permission, three session management methods are available to the bot:
- imopenlines.bot.session.operator — transfer the conversation to the first available operator of the channel, only
CHAT_IDis needed - imopenlines.bot.session.transfer — transfer to a specific employee in the
USER_IDparameter or to a queue in theQUEUE_IDparameter, only one assignment at a time - imopenlines.bot.session.finish — finish the session
The imopenlines.bot.session.transfer and imopenlines.bot.session.finish methods act on behalf of the bot, so the webhook passes in the CLIENT_ID parameter the same botToken that was used during the registration. The imopenlines.bot.session.operator method has no CLIENT_ID parameter.
The LEAVE flag in the imopenlines.bot.session.transfer method defines whether the bot stays in the chat: Y — the bot leaves immediately, N — it stays until the transfer is confirmed. The default value is N.
Collect the reply branches into the handleLinesMessage function that the handler from step 2 calls. The example handles three keywords:
- "operator" — transfer the conversation to the first available employee of the channel
- "manager" — transfer the conversation to the employee from
OPERATOR_ID - "thank you" — say goodbye and finish the session
The bot replies to all the other messages with a hint.
async function handleLinesMessage(chatId, text) {
if (text.includes('operator')) {
await call('imopenlines.bot.session.operator', { CHAT_ID: chatId })
return
}
if (text.includes('manager')) {
await call('imopenlines.bot.session.transfer', {
CHAT_ID: chatId,
USER_ID: operatorId,
LEAVE: 'Y',
CLIENT_ID: botToken,
})
return
}
if (text === 'thank you') {
await sendReply(chatId, 'Happy to help! Feel free to reach out again')
await call('imopenlines.bot.session.finish', {
CHAT_ID: chatId,
CLIENT_ID: botToken,
})
return
}
await sendReply(chatId, 'Hello! Describe your question or type "operator" to connect an employee')
}
def handle_lines_message(chat_id: int, text: str) -> None:
if "operator" in text:
client.imopenlines.bot.session.operator(chat_id=chat_id).response
return
if "manager" in text:
# The typed wrapper does not accept CLIENT_ID, so the method is called through the SDK core
token.call_method(
"imopenlines.bot.session.transfer",
{
"CHAT_ID": chat_id,
"USER_ID": operator_id,
"LEAVE": "Y",
"CLIENT_ID": bot_token,
},
)
return
if text == "thank you":
send_reply(chat_id, "Happy to help! Feel free to reach out again")
token.call_method(
"imopenlines.bot.session.finish",
{"CHAT_ID": chat_id, "CLIENT_ID": bot_token},
)
return
send_reply(chat_id, "Hello! Describe your question or type 'operator' to connect an employee")
function handleLinesMessage(int $chatId, string $text): void
{
global $b24, $botToken, $operatorId;
if (str_contains($text, 'operator')) {
$b24->core->call('imopenlines.bot.session.operator', ['CHAT_ID' => $chatId]);
return;
}
if (str_contains($text, 'manager')) {
$b24->core->call('imopenlines.bot.session.transfer', [
'CHAT_ID' => $chatId,
'USER_ID' => $operatorId,
'LEAVE' => 'Y',
'CLIENT_ID' => $botToken,
]);
return;
}
if ($text === 'thank you') {
sendReply($chatId, 'Happy to help! Feel free to reach out again');
$b24->core->call('imopenlines.bot.session.finish', [
'CHAT_ID' => $chatId,
'CLIENT_ID' => $botToken,
]);
return;
}
sendReply($chatId, 'Hello! Describe your question or type "operator" to connect an employee');
}
Successful response of each session management method:
{
"result": true
}
Check the Result
- Check the registration with the imbot.v2.Bot.list method and the
botTokenparameter — theresult.botsarray contains the bot with yourcode, itsisSupportOpenlineequalstrue, andeventModeequalswebhook - Write to the channel connected to the Open Channel. The handler receives
ONIMBOTV2MESSAGEADD, in whichdata.chat.entityTypeequalsLINES, and the bot replies with the text from step 3 - Write "operator". The
imopenlines.bot.session.operatormethod returnstrue, and an employee of the channel joins the conversation
The whole conversation is visible in the Contact Center > Open Channels section: the session history contains both the bot replies and the moment of the transfer to the operator.
Errors and Diagnostics
If a method returns an error, check the request data and the webhook permissions.
BOT_TOKEN_NOT_SPECIFIED—fields.botTokenwas not passed, it is required when authorizing through a webhookBOT_TOKEN_INVALID_LENGTH— the bot token is longer than 40 characters, shorten theBOT_TOKENvalueBOT_WEBHOOK_URL_REQUIRED—fields.webhookUrlwas not passed for webhook mode, specify the handler URLBOT_INVALID_CALLBACK— an invalid URL was passed infields.webhookUrl, check thehttpsscheme and the domain nameBOT_CODE_ALREADY_TAKEN— the bot code is taken, choose a differentfields.codevalueCHAT_ID_EMPTY—CHAT_IDwas not passed or a value<= 0was passed, takedata.chat.idfrom the eventUSER_ID_EMPTY— an emptyUSER_IDor a value<= 0was passed toimopenlines.bot.session.transfer. This happens when theOPERATOR_IDvariable is not set in the environmentBOT_ID_ERROR— a value with no registered bot behind it was passed inCLIENT_ID, compare it withfields.botTokenfrom step 1ACCESS_DENIED— theCLIENT_IDparameter was not passed at all, or the webhook has noimopenlinespermission, check both conditionsWRONG_CHAT— the conversation is already handled by an operator, not by the bot, there is no need to transfer the session againOPERATOR_WRONG— the conversation cannot be transferred to the specified employee or queue, checkUSER_ID
If there is no error but the bot stays silent, check the chain step by step:
- the events do not reach the handler — the bot is not connected to the channel in the Open Channel settings, or
fields.eventModestill holds thefetchvalue. Check the bot with theimbot.v2.Bot.listmethod and repeat step 1 - the events arrive but the handler responds with
403— theBOT_TOKENvalue in the server environment does not match the registration token, compare it withfields.botTokenfrom step 1 - the events arrive but the condition does not fire — compare
data.chat.entityTypewith theLINESstring and keep in mind that in webhook mode all the scalars arrive as strings - the handler responds with something other than
200— the platform does not guarantee repeated delivery of an event, and the conversation stays without a reply
Important Notes
- The methods and events of the
imbot.*branch are deprecated. Useimbot.v2.*for new bots, the migration order is described in the article Migration from imbot to imbot.v2 - A bot registered with the
imbot.*methods receivesONIMBOT*events, and a bot fromimbot.v2.*receivesONIMBOTV2*events - In an Open Channel chat, the bot receives all the customer messages without the
@botmention, unlike in group chats - In an application with OAuth authorization,
fields.botTokenduring the registration andCLIENT_IDduring the session management are not needed: the bot is bound to the application throughclient_id. At the same time, the events do not arrive until the application completes the installation - To adapt the scenario to your task, change only the
handleLinesMessagefunction: the conditions on the text, the reply texts, and the way the conversation is transferred - To route the requests to a queue, pass to
imopenlines.bot.session.transfertheQUEUE_IDparameter with the value of theIDfield from the response of the imopenlines.config.list.get method