How to Embed a Widget into a Lead as a Custom Field

Scope: placement, crm

Who can execute the methods:

  • userfieldtype.add — administrator
  • app.info — any user
  • crm.lead.userfield.add — CRM administrator
  • crm.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:

  1. userfieldtype.add — register a custom field type and the handler URL
  2. app.info — retrieve the App ID and generate the full field type code
  3. crm.lead.userfield.add — create a field in the lead card
  4. crm.item.get — retrieve the lead's phone number in the field handler
  5. 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 specify phone_data

  • HANDLER — the public field handler URL. We will pass https://your-domain.example/handler.php

  • TITLE — the field type name in the settings interface. We will specify Phone data

  • DESCRIPTION — the field type description

  • OPTIONS — additional configurations. In the example, we will set the field height to height: 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 with ID = 123, we will pass rest_123_phone_data

  • FIELD_NAME — the field code without the UF_CRM_ prefix. We will specify PHONE_DATA

  • XML_ID — the external field code. In the example, it matches FIELD_NAME

  • MANDATORY — field mandatory status. We will pass N

  • 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 pass Y

  • 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:

  1. If the field is empty, retrieve the lead phone using the crm.item.get method
  2. Pass the new value to the card form using the setValue method

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
type

Description

MODE
string

Field display mode. The source code uses values edit and view

ENTITY_ID
string

Object code for the card where the field is open. For a lead, CRM_LEAD is received

FIELD_NAME
string

Full name of the custom field with the prefix UF_CRM_. For the field PHONE_DATA, UF_CRM_PHONE_DATA will be received

ENTITY_VALUE_ID
string, integer

CRM item identifier. In this scenario, it is the lead identifier. For a new card, it may have the value 0

VALUE
string, array, null

Current field value. For a single field, one value is received; for a multiple field, an array is received

MULTIPLE
string

Multiple field flag: Y or N

MANDATORY
string

Required field flag: Y or N

XML_ID
string, null

External code of the field

Verify the Widget

  1. Execute userfieldtype.add and ensure the method returned true
  2. Execute app.info, substituting result.ID into the full rest_<APP_ID>_phone_data type code
  3. Create a field using the crm.lead.userfield.add method and add it to the lead card form
  4. Open a saved lead with a populated phone number and check the field value in view mode
  5. Switch to edit mode, change the field value, and save the card
  6. 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.add instead of rest_<APP_ID>_phone_data, or the app installation is not complete
  • If the field does not load, check the HANDLER HTTPS address, its domain, and its availability from the internet
  • A ACCESS_DENIED error in crm.item.get means the user lacks permission to read the lead
  • If a NOT_FOUND error occurs in crm.item.get, check ENTITY_VALUE_ID and the entityTypeId value
  • 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

crm.lead.userfield.add

CRM_LEAD

1

Deal

crm.deal.userfield.add

CRM_DEAL

2

Contact

crm.contact.userfield.add

CRM_CONTACT

3

Company

crm.company.userfield.add

CRM_COMPANY

4

Continue Learning