How to Integrate External Telephony with Bitrix24
- SDK Initialization
- 1. Assemble the Application
- 2. Register an Incoming Call
- 3. Showing a Call to an Employee Group
- 4. Routing a Call to the Customer's Responsible Person
- 5. Handling an Outgoing Call from the CRM
- 6. Finishing a Call and Retaining the Result
- Recording a Call Without Showing a Card
- Continue Learning
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect to the MCP server so that the assistant can utilize the official REST documentation.
External telephony transmits call data from the PBX to Bitrix24: client number, user, line, call status, and recording. Bitrix24 displays the call detail form to the employee, links the call to the CRM, and saves the result in the statistics.
To integrate external telephony, follow these six steps:
- Assemble the application and handlers for the PBX and Bitrix24
- Register an incoming call
- Display the call card to an employee group
- Route the call to the customer's responsible person
- Process an outgoing call from the CRM
- Complete the call and save the result
We will separately examine a scenario where a call must be recorded without displaying a card.
REST methods telephony.externalCall.* and telephony.externalLine.* work both via an incoming webhook and within the context of an application. The ONEXTERNALCALLSTART event (step 5) is sent only to an installed application—it is received by your web server.
In PHP, telephony methods are called directly through the core ($b24->core->call(...)). Typed analogs are available in getTelephonyScope()->externalCall() (show, hide, register, finishForUserId) and ->externalLine(), but they require value objects (CallType, TelephonyCallStatusCode, Money, CarbonImmutable).
SDK Initialization
// npm install @bitrix24/b24jssdk
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl('https://your-domain.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/')
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$b24 = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/');
# pip install b24pysdk
from b24pysdk import Client, BitrixWebhook
client = Client(BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="1/xxxxxxxxxxxxxxxx",
))
1. Assemble the Application
A working integration typically consists of a server-side application and handlers for the PBX and Bitrix24:
- Create a local application or a Market application
- Complete the application installation and retain the authorization
- Register an external line using the telephony.externalLine.add method. Pass the line number in
LINE_NUMBERof the telephony.externalCall.register method - Subscribe the application to ONEXTERNALCALLSTART using the event.bind method if you need to initiate outgoing calls from the CRM
- Create an event handler from the PBX that calls
telephony.externalCall.register/show/hide/finishbased on the call status - Create an ONEXTERNALCALLSTART handler for outgoing calls
- If the call recording appears after completion, attach it using the telephony.externalCall.attachRecord method
Registering an external line:
const response = await $b24.actions.v2.call.make({
method: 'telephony.externalLine.add',
params: { NUMBER: 'line-1', NAME: 'External line' },
requestId: 'line-add',
})
$b24->core->call('telephony.externalLine.add', [
'NUMBER' => 'line-1',
'NAME' => 'External line',
]);
client.telephony.external_line.add(number="line-1", name="External line").response
2. Register an Incoming Call
When the PBX receives an incoming call, call telephony.externalCall.register:
USER_ID— the employee to whom the card should be displayedPHONE_NUMBER— the customer numberTYPE = 2— an incoming callLINE_NUMBER— the external line numberEXTERNAL_CALL_ID— the unique ID of the call on the PBX sideSHOW = 1(or do not pass) — the card will open for the user fromUSER_ID
The method returns CALL_ID for further actions (show, hide, finish, attachRecord).
const response = await $b24.actions.v2.call.make({
method: 'telephony.externalCall.register',
params: {
USER_ID: 1269,
PHONE_NUMBER: '499062195047',
TYPE: 2,
LINE_NUMBER: 'line-1',
EXTERNAL_CALL_ID: 'asterisk-1773130778.18441',
SHOW: 1,
},
requestId: 'call-register',
})
const callId = response.getData().result.CALL_ID
$response = $b24->core->call('telephony.externalCall.register', [
'USER_ID' => 1269,
'PHONE_NUMBER' => '499062195047',
'TYPE' => 2,
'LINE_NUMBER' => 'line-1',
'EXTERNAL_CALL_ID' => 'asterisk-1773130778.18441',
'SHOW' => 1,
]);
$callId = $response->getResponseData()->getResult()['CALL_ID'];
bitrix_response = client.telephony.external_call.register(
phone_number="499062195047",
call_type=2,
user_id=1269,
line_number="line-1",
external_call_id="asterisk-1773130778.18441",
show=1,
).response
call_id = bitrix_response.result["CALL_ID"]
3. Showing a Call to an Employee Group
Simultaneous Queue. Pass an array of employee identifiers to the USER_ID parameter of the telephony.externalCall.show method. When the operator answers, hide the card for the others using the telephony.externalCall.hide method.
In the example, the card is shown to three employees, and then, when employee 1270 answers, it is hidden for the others.
const queue = [1269, 1270, 1271]
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.show',
params: { CALL_ID: callId, USER_ID: queue },
requestId: 'call-show',
})
const answeredUserId = 1270
const usersToHide = queue.filter((userId) => userId !== answeredUserId)
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.hide',
params: { CALL_ID: callId, USER_ID: usersToHide },
requestId: 'call-hide',
})
$queue = [1269, 1270, 1271];
// Typed analog: $b24->getTelephonyScope()->externalCall()->show($callId, $queue);
$b24->core->call('telephony.externalCall.show', [
'CALL_ID' => $callId,
'USER_ID' => $queue,
]);
$answeredUserId = 1270;
$usersToHide = array_values(array_filter($queue, fn($userId) => $userId !== $answeredUserId));
$b24->core->call('telephony.externalCall.hide', [
'CALL_ID' => $callId,
'USER_ID' => $usersToHide,
]);
queue = [1269, 1270, 1271]
client.telephony.external_call.show(call_id=call_id, user_id=queue).response
answered_user_id = 1270
users_to_hide = [uid for uid in queue if uid != answered_user_id]
client.telephony.external_call.hide(call_id=call_id, user_id=users_to_hide).response
Sequential Queue. Show the card to the first employee using method show. If he does not answer within the time specified in the PBX, hide the card using method hide and show it to the next employee using method show.
4. Routing a Call to the Customer's Responsible Person
To show a call to the responsible manager, register the call with SHOW = 0. Bitrix24 will find the CRM object by number and return CRM_ENTITY_TYPE and CRM_ENTITY_ID. Get the responsible person from the object and pass it to telephony.externalCall.show.
const reg = await $b24.actions.v2.call.make({
method: 'telephony.externalCall.register',
params: { PHONE_NUMBER: '499062195047', TYPE: 2, LINE_NUMBER: 'line-1', SHOW: 0 },
requestId: 'call-register',
})
const { CALL_ID, CRM_ENTITY_TYPE, CRM_ENTITY_ID } = reg.getData().result
let assignedById
if (CRM_ENTITY_TYPE === 'CONTACT' && CRM_ENTITY_ID) {
const contact = await $b24.actions.v2.call.make({
method: 'crm.contact.get', params: { id: CRM_ENTITY_ID }, requestId: 'contact-get',
})
assignedById = contact.getData().result.ASSIGNED_BY_ID
}
if (assignedById) {
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.show',
params: { CALL_ID, USER_ID: assignedById },
requestId: 'call-show',
})
}
$reg = $b24->core->call('telephony.externalCall.register', [
'PHONE_NUMBER' => '499062195047', 'TYPE' => 2, 'LINE_NUMBER' => 'line-1', 'SHOW' => 0,
])->getResponseData()->getResult();
$assignedById = null;
if (($reg['CRM_ENTITY_TYPE'] ?? '') === 'CONTACT' && !empty($reg['CRM_ENTITY_ID'])) {
$contact = $b24->getCRMScope()->contact()->get((int)$reg['CRM_ENTITY_ID'])->contact();
$assignedById = $contact->ASSIGNED_BY_ID;
}
if ($assignedById) {
$b24->core->call('telephony.externalCall.show', [
'CALL_ID' => $reg['CALL_ID'],
'USER_ID' => [$assignedById],
]);
}
reg = client.telephony.external_call.register(
phone_number="499062195047", call_type=2, line_number="line-1", show=0,
).response.result
assigned_by_id = None
if reg.get("CRM_ENTITY_TYPE") == "CONTACT" and reg.get("CRM_ENTITY_ID"):
contact = client.crm.contact.get(bitrix_id=reg["CRM_ENTITY_ID"]).response.result
assigned_by_id = contact["ASSIGNED_BY_ID"]
if assigned_by_id:
client.telephony.external_call.show(call_id=reg["CALL_ID"], user_id=assigned_by_id).response
To find a customer by phone number without registering a call, use telephony.externalCall.searchCrmEntities.
5. Handling an Outgoing Call from the CRM
When an employee clicks on a number in the CRM, Bitrix24 registers the call and sends the ONEXTERNALCALLSTART event to the application with fields CALL_ID, PHONE_NUMBER, USER_ID, LINE_NUMBER, CRM_ENTITY_TYPE, CRM_ENTITY_ID, and CALL_LIST_ID.
Your web server receives the event (the SDK only performs outgoing calls). After initiating the call on the PBX, complete the same CALL_ID using method finish.
import express from 'express'
const app = express()
app.use(express.urlencoded({ extended: true }))
app.post('/events', async (req, res) => {
if (req.body.event === 'ONEXTERNALCALLSTART') {
const data = req.body.data
// ... initiate call to PBX via data.PHONE_NUMBER ...
// upon completion of the call:
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.finish',
params: { CALL_ID: data.CALL_ID, USER_ID: data.USER_ID, DURATION: 95, STATUS_CODE: '200' },
requestId: 'call-finish',
})
}
res.send('ok')
})
<?php
// ONEXTERNALCALLSTART event handler
if (($_REQUEST['event'] ?? '') === 'ONEXTERNALCALLSTART') {
$data = $_REQUEST['data'];
// ... initiate call to PBX via $data['PHONE_NUMBER'] ...
$b24->core->call('telephony.externalCall.finish', [
'CALL_ID' => $data['CALL_ID'],
'USER_ID' => $data['USER_ID'],
'DURATION' => 95,
'STATUS_CODE' => '200',
]);
}
from flask import Flask, request
app = Flask(__name__)
@app.post("/events")
def events():
if request.form.get("event") == "ONEXTERNALCALLSTART":
data = request.form # fields arrive as data[CALL_ID] etc..
# ... initiate call to PBX ...
client.telephony.external_call.finish(
call_id=data.get("data[CALL_ID]"),
user_id=int(data.get("data[USER_ID]")),
duration=95,
status_code="200",
).response
return "ok"
6. Finishing a Call and Retaining the Result
After the conversation, call telephony.externalCall.finish: the method hides the card, retains the call in statistics, and creates a CRM activity. Pass CALL_ID, USER_ID, DURATION (sec), and STATUS_CODE (200 — successful, 304 — missed).
If the recording is not yet ready, call finish without a recording, and attach it later using the telephony.externalCall.attachRecord method. Once the recording is available, you can add a transcription using the telephony.call.attachTranscription method.
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.finish',
params: { CALL_ID: callId, USER_ID: 1270, DURATION: 95, STATUS_CODE: '200', ADD_TO_CHAT: 1 },
requestId: 'call-finish',
})
// later, when the recording is ready
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.attachRecord',
params: { CALL_ID: callId, FILENAME: 'record.mp3', RECORD_URL: 'https://your-domain.example/record.mp3' },
requestId: 'attach-record',
})
$b24->core->call('telephony.externalCall.finish', [
'CALL_ID' => $callId, 'USER_ID' => 1270, 'DURATION' => 95, 'STATUS_CODE' => '200', 'ADD_TO_CHAT' => 1,
]);
// later, when the recording is ready
$b24->core->call('telephony.externalCall.attachRecord', [
'CALL_ID' => $callId, 'FILENAME' => 'record.mp3', 'RECORD_URL' => 'https://your-domain.example/record.mp3',
]);
client.telephony.external_call.finish(
call_id=call_id, user_id=1270, duration=95, status_code="200", add_to_chat=1,
).response
# later, when the recording is ready
client.telephony.external_call.attach_record(
call_id=call_id, filename="record.mp3", record_url="https://your-domain.example/record.mp3",
).response
Recording a Call Without Showing a Card
If the connection between the PBX and Bitrix24 was unavailable, save the fact of the call without a card after connectivity is restored: call register with SHOW = 0, then finish with the actual data. The scenario does not show the call in real time, but it retains the history, statistics, and CRM activity.
const reg = await $b24.actions.v2.call.make({
method: 'telephony.externalCall.register',
params: { USER_ID: 1269, PHONE_NUMBER: '499062195047', TYPE: 2, LINE_NUMBER: 'line-1', SHOW: 0 },
requestId: 'call-register',
})
const callId = reg.getData().result.CALL_ID
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.finish',
params: { CALL_ID: callId, USER_ID: 1269, DURATION: 0, STATUS_CODE: '304' },
requestId: 'call-finish',
})
$callId = $b24->core->call('telephony.externalCall.register', [
'USER_ID' => 1269, 'PHONE_NUMBER' => '499062195047', 'TYPE' => 2, 'LINE_NUMBER' => 'line-1', 'SHOW' => 0,
])->getResponseData()->getResult()['CALL_ID'];
$b24->core->call('telephony.externalCall.finish', [
'CALL_ID' => $callId, 'USER_ID' => 1269, 'DURATION' => 0, 'STATUS_CODE' => '304',
]);
call_id = client.telephony.external_call.register(
phone_number="499062195047", call_type=2, user_id=1269, line_number="line-1", show=0,
).response.result["CALL_ID"]
client.telephony.external_call.finish(
call_id=call_id, user_id=1269, duration=0, status_code="304",
).response