How to Embed a Widget into a Lead as a Custom Field
Scope:
placement,crmWho can execute the methods:
userfieldtype.add— administratorapp.info— any usercrm.lead.userfield.add— CRM administratorcrm.item.get— any user with lead read permissions
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.
A custom field type allows you to display an application interface directly within a lead card. In this scenario, we will create a field with the code PHONE_DATA without the UF_CRM_ prefix.
If the field is empty, the handler receives the card context, reads the lead's phone number, and passes the found value into the form.
To embed a widget into a lead field, perform the following methods and commands in sequence:
- userfieldtype.add — register a custom field type and the handler URL
- app.info — retrieve the App ID and generate the full field type code
- crm.lead.userfield.add — create a field in the lead card
- crm.item.get — retrieve the lead's phone number in the field handler
- setValue — pass the new field value into the card form
The scenario requires an application context: the userfieldtype.* methods will register the field type, and app.info will return the application's ID. An incoming webhook will not work.
How the Scenario Works
During registration, the userfieldtype.add method saves the handler for the private embedding point USERFIELD_TYPE.
When a user opens a lead card containing a field of this type, Bitrix24 opens the handler URL inside the field and passes PLACEMENT_OPTIONS to it. In edit mode, the handler can change the field value by calling:
$b24.placement.setValue(value)
The setValue method accepts the field value itself and writes it to a hidden field in the card form. In a lead, the value will be retained after the card is saved. In view mode, the handler can only display the interface or a text value.
Prepare the Handler
Create an application page with a public address. This address is required for the HANDLER parameter of the userfieldtype.add method. We recommend using HTTPS so that the browser does not block the loading of the field content.
The handler address must use the http or https protocol and include a domain.
The following address is used in the examples below:
https://your-domain.example/handler.php
All calls are executed within the context of the installed application. The application receives authorization (access_token, domain) during installation and in every handler call. Below is how to initialize the SDK in this context:
// npm install @bitrix24/b24jssdk
// The application page opens inside a Bitrix24 iframe
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
// ... calls to $b24.actions.v2.call.make(...) and $b24.placement.*
// at the end of the page operation: $b24.destroy()
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Core\Credentials\ApplicationProfile;
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\HttpFoundation\Request;
$appProfile = ApplicationProfile::initFromArray([
'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => 'local.xxxxxxxx.xxxxxxxx',
'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => 'yyyyyyyy',
'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'crm,placement',
]);
// Bitrix24 passes the DOMAIN and the application token in the handler request
$b24 = ServiceBuilderFactory::createServiceBuilderFromPlacementRequest(
Request::createFromGlobals(),
$appProfile
);
# pip install b24pysdk
from b24pysdk import BitrixApp, BitrixToken, Client
bitrix_app = BitrixApp(
client_id="local.xxxxxxxx.xxxxxxxx",
client_secret="yyyyyyyy",
)
# auth arrives in the installation or application call request
client = Client(BitrixToken(
domain=auth["domain"],
auth_token=auth["access_token"],
refresh_token=auth["refresh_token"],
bitrix_app=bitrix_app,
))
1. Register a Field Type
Register a field type using the userfieldtype.add method. Specify the type code, handler URL, and field display configurations.
-
USER_TYPE_ID— the string type code. We will specifyphone_data -
HANDLER— the public field handler URL. We will passhttps://your-domain.example/handler.php -
TITLE— the field type name in the settings interface. We will specifyPhone data -
DESCRIPTION— the field type description -
OPTIONS— additional configurations. In the example, we will set the field height toheight: 60
const handlerUrl = 'https://your-domain.example/handler.php'
const userTypeId = 'phone_data'
const response = await $b24.actions.v2.call.make({
method: 'userfieldtype.add',
params: {
USER_TYPE_ID: userTypeId,
HANDLER: handlerUrl,
TITLE: 'Phone data',
DESCRIPTION: 'Lead phone data field',
OPTIONS: {
height: 60,
},
},
requestId: 'userfieldtype-add',
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
console.info('User field type registered')
<?php
$handlerUrl = 'https://your-domain.example/handler.php';
$userTypeId = 'phone_data';
// The typed analog does not accept OPTIONS:
// $b24->getPlacementScope()->userfieldtype()->add($userTypeId, $handlerUrl, 'Phone data', 'Lead phone data field');
// To pass OPTIONS (height), call the method directly via the core:
$response = $b24->core->call('userfieldtype.add', [
'USER_TYPE_ID' => $userTypeId,
'HANDLER' => $handlerUrl,
'TITLE' => 'Phone data',
'DESCRIPTION' => 'Lead phone data field',
'OPTIONS' => ['height' => 60],
]);
// core->call wraps the scalar result in an array
$isRegistered = $response->getResponseData()->getResult()[0];
echo $isRegistered ? 'User field type registered' : 'Error';
bitrix_response = client.userfieldtype.add(
"phone_data",
"https://your-domain.example/handler.php",
title="Phone data",
description="Lead phone data field",
options={"height": 60},
).response
print("User field type registered" if bitrix_response.result else "Error")
If the field type is successfully registered, the method will return true. If an error is received error, review the possible error descriptions in the userfieldtype.add method documentation.
{
"result": true,
"time": {
"start": 1724421710.397825,
"finish": 1724421711.040353,
"duration": 0.6425280570983887,
"processing": 0.00005888938903808594,
"date_start": "2024-08-23T16:01:50+02:00",
"date_finish": "2024-08-23T16:01:51+02:00",
"operating": 0
}
}
The method registers a type with the short code phone_data. To create a field in the CRM, the full code of the following type will be required rest_<APP_ID>_phone_data.
2. Retrieve the App ID
Retrieve the App ID using the app.info method. The method does not accept parameters. The field ID will be required in the response.
const response = await $b24.actions.v2.call.make({
method: 'app.info',
params: {},
requestId: 'app-info',
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
const applicationId = response.getData().result.ID
const fullUserTypeId = `rest_${applicationId}_phone_data`
console.info('Full user type ID: ' + fullUserTypeId)
<?php
use Bitrix24\SDK\Core\Exceptions\BaseException;
try
{
$applicationId = $b24->getMainScope()->main()->getApplicationInfo()->applicationInfo()->ID;
$fullUserTypeId = 'rest_' . $applicationId . '_phone_data';
echo 'Full user type ID: ' . $fullUserTypeId;
}
catch (BaseException $exception)
{
echo $exception->getMessage();
}
from b24pysdk.errors import BitrixAPIError
try:
application_id = client.app.info().response.result["ID"]
full_user_type_id = f"rest_{application_id}_phone_data"
print("Full user type ID:", full_user_type_id)
except BitrixAPIError as error:
print(error)
For an application with ID = 123, the full type code will be rest_123_phone_data.
Response fragment:
{
"result": {
"ID": 123,
"INSTALLED": true
}
}
If INSTALLED is set to false, complete the application installation — there is a method in B24JsSDK for this $b24.installFinish(). For more details, see the BX24.installFinish description.
3. Create a Lead Field
Create a lead custom field using the crm.lead.userfield.add method. Specify the field configurations in the fields object.
-
USER_TYPE_ID— the full code of the registered field type. For an application withID = 123, we will passrest_123_phone_data -
FIELD_NAME— the field code without theUF_CRM_prefix. We will specifyPHONE_DATA -
XML_ID— the external field code. In the example, it matchesFIELD_NAME -
MANDATORY— field mandatory status. We will passN -
SHOW_IN_LIST— whether to show the field in the list. In the CRM, this parameter does not affect field display, but we will include it in the request as a standard custom field parameter -
EDIT_IN_LIST— whether the field can be edited. We will passY -
EDIT_FORM_LABEL— the field e-Signature in the lead card -
LIST_COLUMN_LABEL— the field heading in the list -
SETTINGS— configurations for the created CRM custom field. For a custom type, we will pass an empty object
const applicationId = 123
const registeredUserTypeId = 'phone_data'
const userTypeId = `rest_${applicationId}_${registeredUserTypeId}`
const fieldName = 'PHONE_DATA'
const response = await $b24.actions.v2.call.make({
method: 'crm.lead.userfield.add',
params: {
fields: {
USER_TYPE_ID: userTypeId,
FIELD_NAME: fieldName,
XML_ID: fieldName,
MANDATORY: 'N',
SHOW_IN_LIST: 'Y',
EDIT_IN_LIST: 'Y',
EDIT_FORM_LABEL: 'Phone data',
LIST_COLUMN_LABEL: 'Phone data',
SETTINGS: {},
},
},
requestId: 'lead-userfield-add',
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
console.info('Lead field created, ID: ' + response.getData().result)
<?php
use Bitrix24\SDK\Core\Exceptions\BaseException;
$applicationId = 123;
$registeredUserTypeId = 'phone_data';
$userTypeId = 'rest_' . $applicationId . '_' . $registeredUserTypeId;
$fieldName = 'PHONE_DATA';
try
{
$fieldId = $b24->getCRMScope()->leadUserfield()->add([
'USER_TYPE_ID' => $userTypeId,
'FIELD_NAME' => $fieldName,
'XML_ID' => $fieldName,
'MANDATORY' => 'N',
'SHOW_IN_LIST' => 'Y',
'EDIT_IN_LIST' => 'Y',
'EDIT_FORM_LABEL' => 'Phone data',
'LIST_COLUMN_LABEL' => 'Phone data',
'SETTINGS' => [],
])->getId();
echo 'Lead field created, ID: ' . $fieldId;
}
catch (BaseException $exception)
{
echo $exception->getMessage();
}
from b24pysdk.errors import BitrixAPIError
application_id = 123
registered_user_type_id = "phone_data"
user_type_id = f"rest_{application_id}_{registered_user_type_id}"
field_name = "PHONE_DATA"
try:
bitrix_response = client.crm.lead.userfield.add(
fields={
"USER_TYPE_ID": user_type_id,
"FIELD_NAME": field_name,
"XML_ID": field_name,
"MANDATORY": "N",
"SHOW_IN_LIST": "Y",
"EDIT_IN_LIST": "Y",
"EDIT_FORM_LABEL": "Phone data",
"LIST_COLUMN_LABEL": "Phone data",
"SETTINGS": {},
},
).response
print("Lead field created, ID:", bitrix_response.result)
except BitrixAPIError as error:
print(error)
If the field is successfully created, the method will return its identifier. If an error error is received, review the possible error descriptions in the crm.lead.userfield.add method documentation.
{
"result": 6997,
"time": {
"start": 1753789240.8146,
"finish": 1753789241.058695,
"duration": 0.2440950870513916,
"processing": 0.19217395782470703,
"date_start": "2025-07-29T14:40:40+03:00",
"date_finish": "2025-07-29T14:40:41+03:00",
"operating_reset_at": 1753789840,
"operating": 0.19216084480285645
}
}
After creation, the field will appear in the list of lead custom fields. To see it in the card, add the field to the lead card form.
4. Handle the Field Call
When a user opens a lead card, Bitrix24 calls the handler with PLACEMENT=USERFIELD_TYPE. The PLACEMENT_OPTIONS receives the custom field parameters and the current lead identifier.
The handler performs two actions:
- If the field is empty, retrieve the lead phone using the crm.item.get method
- Pass the new value to the card form using the
setValuemethod
For the lead in crm.item.get, specify entityTypeId: 1. In the id parameter, pass the lead identifier from PLACEMENT_OPTIONS.ENTITY_VALUE_ID. If the card is already saved, ENTITY_VALUE_ID contains the lead identifier. For a new card, the value may be 0.
If the field already contains a value, the handler will display it without reloading the phone number.
Only code executing within the field's iframe can write a value to the card form, so setValue is called from JS. In PHP and Python implementations, the server retrieves the lead phone and returns a ready-made page, while a small JS fragment on that page performs the value writing.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Phone data</title>
</head>
<body style="margin: 0; padding: 0;">
<div id="field-content"></div>
<script type="module">
// npm install @bitrix24/b24jssdk
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
const options = $b24.placement.options
const container = document.getElementById('field-content')
if ($b24.placement.placement !== 'USERFIELD_TYPE') {
container.textContent = 'Failed to determine the embedding type'
} else {
let value = options.VALUE || ''
const leadId = Number(options.ENTITY_VALUE_ID)
if (
value === ''
&& options.ENTITY_ID === 'CRM_LEAD'
&& Number.isInteger(leadId)
&& leadId > 0
) {
const response = await $b24.actions.v2.call.make({
method: 'crm.item.get',
params: {
entityTypeId: 1,
id: leadId,
},
requestId: 'lead-get',
})
if (!response.isSuccess) {
console.error(response.getErrorMessages().join('; '))
} else {
const item = response.getData().result.item
const phone = (item?.fm || [])
.find((field) => field.typeId === 'PHONE' && field.value)
?.value
?.trim()
|| item?.phone?.trim()
|| ''
value = phone ? 'Lead phone: ' + phone : 'Phone is empty'
}
}
renderValue(value)
}
function renderValue(value) {
document.body.style.backgroundColor =
options.MODE === 'edit' ? '#fff' : '#f9fafb'
if (options.MODE === 'edit') {
container.innerHTML =
'<input id="phone-data" type="text" style="width: 90%;" />'
const input = document.getElementById('phone-data')
input.value = value
input.addEventListener('keyup', () => $b24.placement.setValue(input.value))
$b24.placement.setValue(value)
} else {
container.textContent = value
}
}
</script>
</body>
</html>
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Core\Credentials\ApplicationProfile;
use Bitrix24\SDK\Core\Exceptions\BaseException;
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$placement = (string)$request->request->get('PLACEMENT', '');
$placementOptions = json_decode(
(string)$request->request->get('PLACEMENT_OPTIONS', '{}'),
true
);
if ($placement !== 'USERFIELD_TYPE' || !is_array($placementOptions))
{
exit;
}
$value = (string)($placementOptions['VALUE'] ?? '');
$errorMessage = '';
if (
$value === ''
&& ($placementOptions['ENTITY_ID'] ?? '') === 'CRM_LEAD'
&& (int)($placementOptions['ENTITY_VALUE_ID'] ?? 0) > 0
)
{
$appProfile = ApplicationProfile::initFromArray([
'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => 'local.xxxxxxxx.xxxxxxxx',
'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => 'yyyyyyyy',
'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'crm,placement',
]);
try
{
// The SDK will take the DOMAIN and the token of the user who opened the card from the request
$b24 = ServiceBuilderFactory::createServiceBuilderFromPlacementRequest(
$request,
$appProfile
);
$item = $b24->getCRMScope()->item()->get(
1,
(int)$placementOptions['ENTITY_VALUE_ID']
)->item();
$phone = '';
foreach (($item->fm ?? []) as $field)
{
if (
($field['typeId'] ?? '') === 'PHONE'
&& trim((string)($field['value'] ?? '')) !== ''
)
{
$phone = trim((string)$field['value']);
break;
}
}
$value = $phone !== '' ? 'Lead phone: ' . $phone : 'Phone is empty';
}
catch (BaseException $exception)
{
$errorMessage = $exception->getMessage();
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Phone data</title>
</head>
<body style="margin: 0; padding: 0; background-color: <?=($placementOptions['MODE'] ?? '') === 'edit' ? '#fff' : '#f9fafb'?>;">
<?php if ($errorMessage !== ''): ?>
<div><?=htmlspecialchars($errorMessage, ENT_QUOTES, 'UTF-8')?></div>
<?php endif; ?>
<?php if (($placementOptions['MODE'] ?? '') === 'edit'): ?>
<input
id="phone-data"
type="text"
style="width: 90%;"
value="<?=htmlspecialchars($value, ENT_QUOTES, 'UTF-8')?>"
>
<script type="module">
// The code inside the iframe field writes the value into the card form
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
const input = document.getElementById('phone-data')
input.addEventListener('keyup', () => $b24.placement.setValue(input.value))
$b24.placement.setValue(input.value)
</script>
<?php else: ?>
<?=htmlspecialchars($value, ENT_QUOTES, 'UTF-8')?>
<?php endif; ?>
</body>
</html>
# pip install b24pysdk flask
from flask import Flask, request
from b24pysdk import BitrixApp, BitrixToken, Client
from b24pysdk.errors import BitrixAPIError
from markupsafe import escape
import json
app = Flask(__name__)
bitrix_app = BitrixApp(
client_id="local.xxxxxxxx.xxxxxxxx",
client_secret="yyyyyyyy",
)
@app.post("/handler")
def handler():
placement = request.form.get("PLACEMENT", "")
options = json.loads(request.form.get("PLACEMENT_OPTIONS", "{}") or "{}")
if placement != "USERFIELD_TYPE":
return ""
value = options.get("VALUE") or ""
error_message = ""
lead_id = int(options.get("ENTITY_VALUE_ID", 0))
if value == "" and options.get("ENTITY_ID") == "CRM_LEAD" and lead_id > 0:
# Bitrix24 passes the domain and the user token to the handler
client = Client(
BitrixToken(
domain=request.args.get("DOMAIN", ""),
auth_token=request.form.get("AUTH_ID", ""),
bitrix_app=bitrix_app,
)
)
try:
item = client.crm.item.get(
entity_type_id=1,
bitrix_id=lead_id,
).response.result["item"]
phone = next(
(
field["value"].strip()
for field in item.get("fm") or []
if field.get("typeId") == "PHONE" and (field.get("value") or "").strip()
),
(item.get("phone") or "").strip(),
)
value = f"Lead phone: {phone}" if phone else "Phone is empty"
except BitrixAPIError as error:
error_message = str(error)
background = "#fff" if options.get("MODE") == "edit" else "#f9fafb"
if options.get("MODE") == "edit":
# The code inside the iframe field writes the value into the card form
script = """<script type="module">
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
const input = document.getElementById('phone-data')
input.addEventListener('keyup', () => $b24.placement.setValue(input.value))
$b24.placement.setValue(input.value)
</script>"""
body = f"""
<input id="phone-data" type="text" style="width: 90%;" value="{escape(value)}">
{script}
"""
else:
body = escape(value)
return f"""<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Phone data</title></head>
<body style="margin: 0; padding: 0; background-color: {background};">
{f'<div>{escape(error_message)}</div>' if error_message else ''}
{body}
</body>
</html>"""
The crm.item.get method returns a item object containing lead fields. In the example, the phone is retrieved from the fm array, which contains multiple fields: phones, e-mail, sites, and messengers.
{
"result": {
"item": {
"id": 123,
"phone": "+499990000000",
"fm": [
{
"id": 456,
"valueType": "WORK",
"value": "+499990000000",
"typeId": "PHONE"
}
]
}
}
}
What the Handler Receives
In the PLACEMENT_OPTIONS HTTP request, the data is passed as a JSON string. In the B24JsSDK, the $b24.placement.options property returns this data as an object. In PHP and Python, you must manually convert the JSON string from the request—for example, using the json_decode or json.loads function.
|
Field |
Description |
|
MODE |
Field display mode. The source code uses values |
|
ENTITY_ID |
Object code for the card where the field is open. For a lead, |
|
FIELD_NAME |
Full name of the custom field with the prefix |
|
ENTITY_VALUE_ID |
CRM item identifier. In this scenario, it is the lead identifier. For a new card, it may have the value |
|
VALUE |
Current field value. For a single field, one value is received; for a multiple field, an array is received |
|
MULTIPLE |
Multiple field flag: |
|
MANDATORY |
Required field flag: |
|
XML_ID |
External code of the field |
Verify the Widget
- Execute
userfieldtype.addand ensure the method returnedtrue - Execute
app.info, substitutingresult.IDinto the fullrest_<APP_ID>_phone_datatype code - Create a field using the
crm.lead.userfield.addmethod and add it to the lead card form - Open a saved lead with a populated phone number and check the field value in view mode
- Switch to edit mode, change the field value, and save the card
- Reopen the lead and ensure the new value has been retained
If the scenario does not work:
- An "Invalid custom type specified" error means that a short code was passed in
crm.lead.userfield.addinstead ofrest_<APP_ID>_phone_data, or the app installation is not complete - If the field does not load, check the
HANDLERHTTPS address, its domain, and its availability from the internet - A
ACCESS_DENIEDerror incrm.item.getmeans the user lacks permission to read the lead - If a
NOT_FOUNDerror occurs incrm.item.get, checkENTITY_VALUE_IDand theentityTypeIdvalue - If the field interface does not launch, check the SDK connection and ensure the client-side code executes after
initializeB24Frame()
How to Adapt a Scenario for Other CRM Cards
To embed the same field into another CRM card, replace the field creation method, the ENTITY_ID check, and the entityTypeId value in crm.item.get.
|
CRM Card |
Field creation method |
ENTITY_ID |
entityTypeId |
|
Lead |
|
|
|
|
Deal |
|
|
|
|
Contact |
|
|
|
|
Company |
|
|