How to Integrate External Telephony with Bitrix24
- Before You Start
- 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
- Check the Result
- Errors and Diagnostics
- What to Consider
- Continue Learning
Scope:
telephonyWho can execute methods: to complete the full scenario, you need an installed application with OAuth authorization
- telephony.externalLine.add, telephony.externalCall.register, telephony.externalCall.finish, and event.bind — the user under whom the application received OAuth authorization
- telephony.externalCall.searchCrmEntities, telephony.externalCall.show, telephony.externalCall.hide, telephony.externalCall.attachRecord, and telephony.call.attachTranscription — any user
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
External telephony transmits call data from the PBX to Bitrix24: client number, user, line, call status, and recording. Bitrix24 displays the call card to the employee, links the call to the CRM, and retains the result in statistics.
The scenario consists of 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
- Finish the call and retain the result
We will separately examine a scenario where a call must be recorded without displaying a card.
The telephony.externalLine.add, telephony.externalCall.register, and telephony.externalCall.finish methods work only in the application context. An inbound webhook is not suitable for them. The ONEXTERNALCALLSTART event (step 5) 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).
Before You Start
Before you begin, make sure you have:
- a local or mass-market application with the
telephonyscope and retained OAuth authorization - a public HTTPS application handler if you need to receive the
ONEXTERNALCALLSTARTevent. An inbound webhook does not receive this event - the employee identifier
USER_IDto whom the call card will be shown - the external line number
LINE_NUMBER, for exampleline-1 - the unique call identifier on the PBX side,
EXTERNAL_CALL_ID - a public URL of the call recording if the recording is passed using the
telephony.externalCall.attachRecordmethod
Replace your-domain.bitrix24.com, 1269, 1270, 1271, line-1, asterisk-1773130778.18441, and the recording URL with the data of your Bitrix24, employees, lines, and PBX.
How to Use Examples in Documentation
SDK Initialization
In the examples below, $b24 for JS, $b24 for PHP, and client for Python are initialized clients with the OAuth token of the installed application. Receiving, storing, and refreshing OAuth tokens are described in Full OAuth 2.0 Authorization Protocol.
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 creates a number that links calls to the application. Pass this number in LINE_NUMBER when registering a call.
const response = await $b24.actions.v2.call.make({
method: 'telephony.externalLine.add',
params: { NUMBER: 'line-1', NAME: 'External line' },
requestId: 'line-add',
})
client.telephony.external_line.add(number="line-1", name="External line").response
$b24->core->call('telephony.externalLine.add', [
'NUMBER' => 'line-1',
'NAME' => 'External line',
]);
The successful response contains the identifier of the created line.
{
"result": {
"ID": 7
}
}
If you need to handle outgoing calls from the CRM, subscribe the application to the ONEXTERNALCALLSTART event. Pass the public HTTPS URL of your handler in handler.
await $b24.actions.v2.call.make({
method: 'event.bind',
params: {
event: 'ONEXTERNALCALLSTART',
handler: 'https://your-domain.example/events',
},
requestId: 'event-bind',
})
client.event.bind(
event="ONEXTERNALCALLSTART",
handler="https://your-domain.example/events",
).response
$b24->core->call('event.bind', [
'event' => 'ONEXTERNALCALLSTART',
'handler' => 'https://your-domain.example/events',
]);
Successful subscription returns true.
{
"result": true
}
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
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"]
$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'];
The successful response contains CALL_ID. Retain it: this identifier is required to show, hide, and finish the call and attach the recording.
{
"result": {
"CALL_ID": "externalCall.716f1cb73def9700a23842adf9c4c568.1773130779",
"CRM_CREATED_LEAD": null,
"CRM_CREATED_ENTITIES": [],
"CRM_ENTITY_TYPE": "CONTACT",
"CRM_ENTITY_ID": 797
}
}
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]
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
$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,
]);
The show and hide methods return true if the command to show or hide the card was sent.
{
"result": true
}
Sequential Queue. Show the card to the first employee using method show. If the employee 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, first find the customer by phone number using the telephony.externalCall.searchCrmEntities method. The method returns the found CRM entities and ASSIGNED_BY_ID, the identifier of the responsible employee.
Then register the call using telephony.externalCall.register with the following parameters:
USER_ID—ASSIGNED_BY_IDfrom the search resultPHONE_NUMBER— the customer numberTYPE = 2— an incoming callLINE_NUMBER— the external line numberEXTERNAL_CALL_ID— the unique call identifier on the PBX sideSHOW = 0— do not show the card immediately after registration
After registration, pass CALL_ID and ASSIGNED_BY_ID to telephony.externalCall.show.
const crmSearch = await $b24.actions.v2.call.make({
method: 'telephony.externalCall.searchCrmEntities',
params: { PHONE_NUMBER: '499062195047' },
requestId: 'crm-search',
})
const crmEntities = crmSearch.getData().result
if (!crmEntities.length) {
throw new Error('Customer with this phone number was not found in the CRM')
}
const assignedById = Number(crmEntities[0].ASSIGNED_BY_ID)
const reg = await $b24.actions.v2.call.make({
method: 'telephony.externalCall.register',
params: {
USER_ID: assignedById,
PHONE_NUMBER: '499062195047',
TYPE: 2,
LINE_NUMBER: 'line-1',
EXTERNAL_CALL_ID: 'asterisk-1773130778.18441-manager',
SHOW: 0,
},
requestId: 'call-register',
})
const callId = reg.getData().result.CALL_ID
await $b24.actions.v2.call.make({
method: 'telephony.externalCall.show',
params: { CALL_ID: callId, USER_ID: assignedById },
requestId: 'call-show',
})
crm_entities = client.telephony.external_call.search_crm_entities(
phone_number="499062195047",
).response.result
if not crm_entities:
raise RuntimeError("Customer with this phone number was not found in the CRM")
assigned_by_id = int(crm_entities[0]["ASSIGNED_BY_ID"])
reg = client.telephony.external_call.register(
phone_number="499062195047",
call_type=2,
user_id=assigned_by_id,
line_number="line-1",
external_call_id="asterisk-1773130778.18441-manager",
show=0,
).response.result
client.telephony.external_call.show(
call_id=reg["CALL_ID"],
user_id=assigned_by_id,
).response
$crmEntities = $b24->core->call('telephony.externalCall.searchCrmEntities', [
'PHONE_NUMBER' => '499062195047',
])->getResponseData()->getResult();
if (empty($crmEntities)) {
throw new \RuntimeException('Customer with this phone number was not found in the CRM');
}
$assignedById = (int)$crmEntities[0]['ASSIGNED_BY_ID'];
$reg = $b24->core->call('telephony.externalCall.register', [
'USER_ID' => $assignedById,
'PHONE_NUMBER' => '499062195047',
'TYPE' => 2,
'LINE_NUMBER' => 'line-1',
'EXTERNAL_CALL_ID' => 'asterisk-1773130778.18441-manager',
'SHOW' => 0,
])->getResponseData()->getResult();
$b24->core->call('telephony.externalCall.show', [
'CALL_ID' => $reg['CALL_ID'],
'USER_ID' => $assignedById,
]);
Store CALL_ID from the register response. You need it to finish the call and attach the recording.
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. After initiating the call on the PBX, complete the same CALL_ID using the finish method. The CALL_ID field arrives in the event, so you do not need to call register again for an outgoing call.
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')
})
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"
<?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',
]);
}
The handler must return the HTTP response 200. After that, Bitrix24 treats the event as delivered.
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.
const finishResponse = 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',
})
finish_response = 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
$finishResponse = $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',
]);
The successful finish response contains the call statistics record. The CALL_STATUS, CALL_FAILED_CODE, CRM_ACTIVITY_ID, CRM_ENTITY_TYPE, and CRM_ENTITY_ID fields help verify that the call is finished and linked to the CRM.
{
"result": {
"CALL_ID": "externalCall.716f1cb73def9700a23842adf9c4c568.1773130779",
"PORTAL_USER_ID": 1270,
"PHONE_NUMBER": "499062195047",
"CALL_DURATION": 95,
"CALL_STATUS": 1,
"CALL_FAILED_CODE": "200",
"CRM_ACTIVITY_ID": 7943,
"CRM_ENTITY_TYPE": "CONTACT",
"CRM_ENTITY_ID": 797,
"ID": 7
}
}
If the recording is attached by RECORD_URL, the attachRecord method returns the file identifier.
{
"result": {
"FILE_ID": 9079
}
}
Recording a Call Without Showing a Card
If the connection between the PBX and Bitrix24 was unavailable, record 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',
EXTERNAL_CALL_ID: 'asterisk-1773130778.18441-offline',
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',
})
call_id = client.telephony.external_call.register(
phone_number="499062195047",
call_type=2,
user_id=1269,
line_number="line-1",
external_call_id="asterisk-1773130778.18441-offline",
show=0,
).response.result["CALL_ID"]
client.telephony.external_call.finish(
call_id=call_id, user_id=1269, duration=0, status_code="304",
).response
$callId = $b24->core->call('telephony.externalCall.register', [
'USER_ID' => 1269,
'PHONE_NUMBER' => '499062195047',
'TYPE' => 2,
'LINE_NUMBER' => 'line-1',
'EXTERNAL_CALL_ID' => 'asterisk-1773130778.18441-offline',
'SHOW' => 0,
])->getResponseData()->getResult()['CALL_ID'];
$b24->core->call('telephony.externalCall.finish', [
'CALL_ID' => $callId, 'USER_ID' => 1269, 'DURATION' => 0, 'STATUS_CODE' => '304',
]);
Check the Result
Verify that the integration processed the call:
- during an incoming call, the call card opened for the required employee
- after the operator answered, the card was hidden for the remaining employees in the queue
- after
finish, the call appeared in telephony statistics - a call activity was created in the CRM if the call is linked to a contact, company, lead, or deal
- after
attachRecord, the call recording is available in the call activity
Through REST, the successful result is confirmed by the finish response fields: ID, CALL_STATUS, CALL_FAILED_CODE, CRM_ACTIVITY_ID, CRM_ENTITY_TYPE, and CRM_ENTITY_ID.
Output these fields from the finish response received in step 6.
const finishResult = finishResponse.getData().result
console.table({
ID: finishResult.ID,
CALL_STATUS: finishResult.CALL_STATUS,
CALL_FAILED_CODE: finishResult.CALL_FAILED_CODE,
CRM_ACTIVITY_ID: finishResult.CRM_ACTIVITY_ID,
CRM_ENTITY_TYPE: finishResult.CRM_ENTITY_TYPE,
CRM_ENTITY_ID: finishResult.CRM_ENTITY_ID,
})
finish_result = finish_response.result
for field in [
"ID",
"CALL_STATUS",
"CALL_FAILED_CODE",
"CRM_ACTIVITY_ID",
"CRM_ENTITY_TYPE",
"CRM_ENTITY_ID",
]:
print(field, finish_result.get(field))
$finishResult = $finishResponse->getResponseData()->getResult();
echo 'ID: ' . $finishResult['ID'] . PHP_EOL;
echo 'CALL_STATUS: ' . $finishResult['CALL_STATUS'] . PHP_EOL;
echo 'CALL_FAILED_CODE: ' . $finishResult['CALL_FAILED_CODE'] . PHP_EOL;
echo 'CRM_ACTIVITY_ID: ' . $finishResult['CRM_ACTIVITY_ID'] . PHP_EOL;
echo 'CRM_ENTITY_TYPE: ' . $finishResult['CRM_ENTITY_TYPE'] . PHP_EOL;
echo 'CRM_ENTITY_ID: ' . $finishResult['CRM_ENTITY_ID'] . PHP_EOL;
Errors and Diagnostics
If the method returns an error, check the request data and the authorization context.
|
Error Code or Message |
Cause and action |
|
|
The method was called outside the application context. Verify the application's OAuth authorization and the |
|
|
No employee was passed to |
|
|
An invalid call type was passed to |
|
|
The customer number was not recognized. Pass the phone number in international format without letters |
|
|
A line with this |
|
|
An invalid |
|
|
The call was not found or is already finished. Verify that |
|
|
The recording is being attached before the call is finished. Call |
|
|
|
|
|
The recording was passed in an unsupported format. Use a |
|
|
An invalid event was passed to |
Repeat the scenario from the step where the error occurred. If the error occurs after a successful register, keep using the same CALL_ID until the call is finished.
What to Consider
- Outgoing calls from the CRM require an installed
ONEXTERNALCALLSTARThandler. An inbound webhook does not receive this event - Pass a unique
EXTERNAL_CALL_IDfor each physical call so that a repeatedregistercall does not return an existingCALL_ID - Store the application's OAuth tokens on the server and do not expose them in public client-side code
- The event handler must be reachable over HTTPS and accept POST requests from Bitrix24
- Attach the call recording after
finish, when the call is already saved in statistics