How to Create Your Lead Editing Form

Scope: crm

Who can execute the method: users with administrative access to the CRM section

If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect the MCP server so that the assistant can utilize the official REST documentation.

An example of automatic generation of a lead edit card containing all fields created in Bitrix24 on an application page.

Some field types are not implemented in this example; for fields with unsupported types, the message field not support will be displayed.

Generated form code:

import express from 'express'
        import { B24Hook } from '@bitrix24/b24jssdk'
        
        const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
        // B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
        
        const app = express()
        app.use(express.urlencoded({ extended: true }))
        
        // Builds <input> (or a set for a multiple field)
        function input(p) {
            const count = p.MULTIPLE && p.TYPE !== 'file' ? 2 : 0
            const values = Array.isArray(p.VALUE) ? p.VALUE : []
            let html = ''
            for (let i = 0; i <= count; i++) {
                const value = count > 0 ? (values[i] ?? '') : (p.VALUE ?? '')
                html += '<input class="form-control' + (p.TYPE === 'file' ? '-file' : '') + '"'
                if (p.NAME) html += ` name="${p.NAME}${p.MULTIPLE ? '[]' : ''}"`
                if (p.TYPE) html += ` type="${p.TYPE}"`
                if (p.REQUIRED) html += ' required'
                if (p.DISABLE) html += ' disabled'
                if (p.CHECKED) html += ' checked'
                if (p.MULTIPLE) html += ' multiple'
                if (value) html += ` value="${value}"`
                html += '>'
            }
            return html
        }
        
        // Builds <select> from a "value => label" dictionary
        function select(p, list) {
            if (!list || !Object.keys(list).length) return ''
            let html = '<select class="form-control"'
            if (p.NAME) html += ` name="${p.NAME}${p.MULTIPLE ? '[]' : ''}"`
            if (p.REQUIRED) html += ' required'
            if (p.DISABLE) html += ' disabled'
            if (p.MULTIPLE) html += ' multiple'
            html += '>'
            const value = Array.isArray(p.VALUE) ? p.VALUE.map(String) : [String(p.VALUE ?? '')]
            for (const [key, title] of Object.entries(list)) {
                html += `<option value="${key}"${value.includes(String(key)) ? ' selected' : ''}>${title}</option>`
            }
            return html + '</select>'
        }
        
        // Page with a form: we receive field descriptions and dictionary values, then render the form
        app.get('/', async (req, res) => {
            const id = parseInt(String(req.query.ID ?? '0'), 10) || 0
        
            // Field descriptions and standard list values (sequential calls)
            const arResult = {}
            arResult.FIELDS = (await $b24.actions.v2.call.make({
                method: 'crm.lead.fields', params: {}, requestId: 'lead-fields',
            })).getData().result
            arResult.FIELD_VALUES_SOURCE_ID = (await $b24.actions.v2.call.make({
                method: 'crm.status.list', params: { filter: { ENTITY_ID: arResult.FIELDS.SOURCE_ID.statusType } }, requestId: 'src',
            })).getData().result
            arResult.FIELD_VALUES_STATUS_ID = (await $b24.actions.v2.call.make({
                method: 'crm.status.list', params: { filter: { ENTITY_ID: arResult.FIELDS.STATUS_ID.statusType } }, requestId: 'sts',
            })).getData().result
            arResult.FIELD_VALUES_CURRENCY = (await $b24.actions.v2.call.make({
                method: 'crm.currency.list', params: {}, requestId: 'cur',
            })).getData().result
        
            if (id > 0) {
                // We get the element and values of related objects if it is an edit form
                arResult.ITEM = (await $b24.actions.v2.call.make({
                    method: 'crm.lead.get', params: { id }, requestId: 'item',
                })).getData().result
                if (arResult.ITEM.COMPANY_ID && arResult.ITEM.COMPANY_ID !== '0') {
                    arResult.VALUE_COMPANY_ID = (await $b24.actions.v2.call.make({
                        method: 'crm.company.get', params: { id: arResult.ITEM.COMPANY_ID }, requestId: 'company',
                    })).getData().result
                }
                if (arResult.ITEM.CONTACT_ID && arResult.ITEM.CONTACT_ID !== '0') {
                    arResult.VALUE_CONTACT_ID = (await $b24.actions.v2.call.make({
                        method: 'crm.contact.get', params: { id: arResult.ITEM.CONTACT_ID }, requestId: 'contact',
                    })).getData().result
                }
            }
        
            // Render: builds the appropriate input/select based on each field type
            let sResult = ''
            let sResultCustom = ''
            for (const [key, field] of Object.entries(arResult.FIELDS)) {
                let value = arResult.ITEM?.[key] ?? ''
                let listKey = arResult['FIELD_VALUES_' + key] ?? []
                let list = {}
                let ret = ''
                const params = { NAME: `form[${key}]`, REQUIRED: field.isRequired, DISABLE: field.isReadOnly, MULTIPLE: field.isMultiple, VALUE: value }
                switch (field.type) {
                    case 'crm_status':
                        if (!listKey.length) {
                            listKey = (await $b24.actions.v2.call.make({
                                method: 'crm.status.list', params: { filter: { ENTITY_ID: field.statusType } }, requestId: `st-${key}`,
                            })).getData().result
                        }
                        list = Object.fromEntries(listKey.map((s) => [s.STATUS_ID, s.NAME]))
                        ret = select(params, list)
                        break
                    case 'crm_currency':
                        list = Object.fromEntries(arResult.FIELD_VALUES_CURRENCY.map((c) => [c.CURRENCY, c.FULL_NAME]))
                        ret = select(params, list)
                        break
                    case 'enumeration':
                        if (field.items) list = Object.fromEntries(field.items.map((it) => [it.ID, it.VALUE]))
                        ret = select(params, list)
                        break
                    case 'crm_multifield':
                        if (Array.isArray(value) && value.length) value = value[0].VALUE
                        ret = input({ ...params, MULTIPLE: false, VALUE: value, TYPE: 'text' })
                        break
                    case 'crm_company':
                        ret = input({ ...params, TYPE: 'text' })
                        if (arResult.VALUE_COMPANY_ID && String(value) === String(arResult.VALUE_COMPANY_ID.ID)) {
                            ret += `(${arResult.VALUE_COMPANY_ID.TITLE})`
                        }
                        break
                    case 'crm_contact':
                        ret = input({ ...params, TYPE: 'text' })
                        if (arResult.VALUE_CONTACT_ID && String(value) === String(arResult.VALUE_CONTACT_ID.ID)) {
                            ret += `(${[arResult.VALUE_CONTACT_ID.NAME, arResult.VALUE_CONTACT_ID.LAST_NAME].join(' ')})`
                        }
                        break
                    case 'date':
                        if (value) value = String(value).slice(0, 10)
                        ret = input({ ...params, VALUE: value, TYPE: 'date' })
                        break
                    case 'datetime':
                        if (value) value = String(value).slice(0, 19)
                        ret = input({ ...params, VALUE: value, TYPE: 'datetime-local' })
                        break
                    case 'char':
                        ret = input({ ...params, VALUE: 'Y', CHECKED: value === 'Y', TYPE: 'checkbox' })
                        break
                    case 'boolean':
                        ret = input({ ...params, VALUE: '1', CHECKED: value === 'Y', TYPE: 'checkbox' })
                        break
                    case 'double':
                    case 'integer':
                        ret = input({ ...params, TYPE: 'number' })
                        break
                    case 'user': {
                        ret = input({ ...params, TYPE: 'number' })
                        if (value) {
                            const users = (await $b24.actions.v2.call.make({
                                method: 'user.get', params: { filter: { ID: value } }, requestId: `user-${key}`,
                            })).getData().result
                            if (users.length) {
                                ret += '(' + users.map((u) => [u.NAME, u.LAST_NAME].join(' ')).join(', ') + ')'
                            }
                        }
                        break
                    }
                    case 'money': {
                        const [money, currency] = String(value).split('|')
                        ret = input({ ...params, VALUE: money, TYPE: 'number' })
                        list = Object.fromEntries(arResult.FIELD_VALUES_CURRENCY.map((c) => [c.CURRENCY, c.FULL_NAME]))
                        ret += select({ NAME: `${key}_CURRENCY`, REQUIRED: field.isRequired, DISABLE: field.isReadOnly, MULTIPLE: field.isMultiple, VALUE: currency }, list)
                        break
                    }
                    case 'resourcebooking':
                        ret = 'field not support'
                        break
                    default:
                        ret = input({ ...params, TYPE: 'text' })
                        break
                }
        
                const label = field.formLabel || field.title
                const block = `<div class="col-4 mt-3">${label}: </div><div class="col-6 mt-3">${ret}</div>`
                if (key.startsWith('UF_')) sResultCustom += block
                else sResult += block
            }
        
            res.send(`
                <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
                <div class="container">
                    <form id="auto_form" enctype="multipart/form-data" method="post">
                        ${arResult.ITEM?.ID ? `<input type="hidden" name="form[ID]" value="${arResult.ITEM.ID}">` : ''}
                        <h2>Standard fields</h2>
                        <div class="row">${sResult}</div>
                        <h2>Custom fields</h2>
                        <div class="row">${sResultCustom}</div>
                        <div class="row"><div class="col-sm-10 mt-5">
                            <input type="submit" class="btn btn-primary" value="Submit">
                        </div></div>
                    </form>
                </div>
                <script>
                    document.getElementById('auto_form').addEventListener('submit', async (el) => {
                        el.preventDefault()
                        const response = await fetch('/form', { method: 'POST', body: new FormData(el.currentTarget) })
                        const json = await response.json()
                        if (json.message) alert(json.message)
                    })
                <\/script>
            `)
        })
        
        app.listen(3000)
        
<?php
            // composer require bitrix24/b24phpsdk:"^3.0"
            require_once 'vendor/autoload.php';
        
            use Bitrix24\SDK\Services\ServiceBuilderFactory;
            use Symfony\Component\EventDispatcher\EventDispatcher;
            use Psr\Log\NullLogger;
        
            $sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
                ->initFromWebhook('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/');
            $crm = $sb->getCRMScope();
        
            $ID = intVal($_REQUEST['ID']);
            class CPrintForm
            {
                /**
                 * @var $arParams array params input keys: 'NAME', 'ID', 'TYPE', 'REQUIRED', 'CHECKED', 'DISABLE', 'MULTIPLE', 'VALUE'
                 * @return string html select
                 */
                public static function input($arParams)
                {
                    $count = 0;
                    $i = 0;
                    $sResult = '';
                    if($arParams[ 'MULTIPLE' ] && $arParams[ 'TYPE' ] != 'file')
                    {
                        $count = 2;
                    }
                    $value = $arParams[ 'VALUE' ];
                    while($i <= $count)
                    {
        
                        if($count > 0)
                        {
                            $value = $arParams[ 'VALUE' ][ $i ];
                        }
                        $sResult .= '<input class="form-control' . (($arParams[ 'TYPE' ] == 'file') ? '-file' : '') . '"';
                        if(!empty($arParams[ 'ID' ]))
                        {
                            $sResult .= ' id="' . $arParams[ 'ID' ] . '"';
                        }
                        if(!empty($arParams[ 'NAME' ]))
                        {
                            $sResult .= ' name="' . $arParams[ 'NAME' ] . '' . (($arParams[ 'MULTIPLE' ]) ? '[]' : '') . '"';
                        }
                        if(!empty($arParams[ 'TYPE' ]))
                        {
                            $sResult .= ' type="' . $arParams[ 'TYPE' ] . '"';
                        }
                        if(!empty($arParams[ 'REQUIRED' ]))
                        {
                            $sResult .= ' required';
                        }
                        if(!empty($arParams[ 'DISABLE' ]))
                        {
                            $sResult .= ' disabled';
                        }
                        if(!empty($arParams[ 'CHECKED' ]))
                        {
                            $sResult .= ' checked';
                        }
                        if(!empty($arParams[ 'MULTIPLE' ])) //sometimes work, not for standard type="text"
                        {
                            $sResult .= ' multiple';
                        }
                        if(!empty($arParams[ 'VALUE' ]))
                        {
                            $sResult .= ' value="' . $value . '"';
                        }
                        $sResult .= '>';
                        $i++;
                    }
        
                    return $sResult;
                }
        
                /**
                 * @var $arParams array settings of select params keys: 'NAME', 'ID', 'REQUIRED', 'DISABLE','MULTIPLE', 'VALUE'
                 * @var $arList array of select options where key is value option and value is title
                 * @return string html select
                 */
                public static function select($arParams, $arList)
                {
                    $sResult = '';
                    if(!empty($arList) && is_array($arList))
                    {
                        $sResult .= '<select class="form-control"'
                            . (($arParams[ 'NAME' ]) ? ' name="' . $arParams[ 'NAME' ] . '' . (($arParams[ 'MULTIPLE' ]) ? '[]' : '') . '"' : '')
                            . (($arParams[ 'ID' ]) ? ' id="' . $arParams[ 'ID' ] . '"' : '')
                            . (($arParams[ 'REQUIRED' ]) ? ' required' : '')
                            . (($arParams[ 'DISABLE' ]) ? ' disabled' : '')
                            . (($arParams[ 'MULTIPLE' ]) ? ' multiple' : '')
                            . '>';
                        $value = [];
                        if(is_array($arParams[ 'VALUE' ]))
                        {
                            $value = $arParams[ 'VALUE' ];
                        }
                        else
                        {
                            $value[] = ($arParams[ 'VALUE' ]) ? $arParams[ 'VALUE' ] : '';
                        }
                        foreach($arList as $key => $title)
                        {
                            $sResult .= '<option value="' . $key . '" ' .
                                ((in_array($key, $value)) ? ' selected' : '')
                                . '>' . $title . '</option>';
                        }
                        $sResult .= '</select>';
                    }
                    return $sResult;
                }
            }
        
            // Field descriptions and dictionary values (sequential calls).
            // Dictionaries are immediately converted into "code => label" dictionaries for select.
            $arResult = [];
            $arResult['FIELDS'] = $crm->lead()->fields()->getFieldsDescription();
        
            $arResult['FIELD_VALUES_SOURCE_ID'] = [];
            foreach($crm->status()->list([], ['ENTITY_ID' => $arResult['FIELDS']['SOURCE_ID']['statusType']], [])->getStatuses() as $status)
            {
                $arResult['FIELD_VALUES_SOURCE_ID'][$status->STATUS_ID] = $status->NAME;
            }
        
            $arResult['FIELD_VALUES_STATUS_ID'] = [];
            foreach($crm->status()->list([], ['ENTITY_ID' => $arResult['FIELDS']['STATUS_ID']['statusType']], [])->getStatuses() as $status)
            {
                $arResult['FIELD_VALUES_STATUS_ID'][$status->STATUS_ID] = $status->NAME;
            }
        
            $arResult['FIELD_VALUES_CURRENCY'] = [];
            foreach($crm->currency()->list([])->getCurrencies() as $currency)
            {
                // CURRENCY in B24PhpSDK is a Money\Currency object; the string code is retrieved via getCode()
                $arResult['FIELD_VALUES_CURRENCY'][$currency->CURRENCY->getCode()] = $currency->FULL_NAME;
            }
        
            if($ID > 0)//get item and standard enum field values if is update form
            {
                // The element is read as an array — the set of fields is dynamic (including UF_*)
                // Access to values is done via names from crm.lead.fields.
                $arResult['ITEM'] = iterator_to_array($crm->lead()->get($ID)->lead()->getIterator());
                if(!empty($arResult['ITEM']['COMPANY_ID']))
                {
                    $company = $crm->company()->get((int)$arResult['ITEM']['COMPANY_ID'])->company();
                    $arResult['VALUE_COMPANY_ID'] = ['ID' => $company->ID, 'TITLE' => $company->TITLE];
                }
                if(!empty($arResult['ITEM']['CONTACT_ID']))
                {
                    $contact = $crm->contact()->get((int)$arResult['ITEM']['CONTACT_ID'])->contact();
                    $arResult['VALUE_CONTACT_ID'] = ['ID' => $contact->ID, 'NAME' => $contact->NAME, 'LAST_NAME' => $contact->LAST_NAME];
                }
            }
        
            $sResult = '';
                $sResultCustom = '';
            if(is_array($arResult['FIELDS'])):
                foreach($arResult[ 'FIELDS' ] as $key => $arField)
                {
                    $value = '';
                    $return = '';
                    if(!empty($arResult[ 'ITEM' ][ $key ]))
                    {
                        $value = $arResult[ 'ITEM' ][ $key ];
                    }
                    $arList = (isset($arResult[ 'FIELD_VALUES_' . $key ])) ? $arResult[ 'FIELD_VALUES_' . $key ] : [];
                    switch($arField[ 'type' ])
                    {
                        case 'crm_status':
                            if(empty($arList))
                            {
                                foreach($crm->status()->list([], ['ENTITY_ID' => $arField[ 'statusType' ]], [])->getStatuses() as $status)
                                {
                                    $arList[$status->STATUS_ID] = $status->NAME;
                                }
                            }
        
                            $return = CPrintForm ::select(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value
                                ],
                                $arList);
                            break;
                        case 'crm_currency':
                            $return = CPrintForm ::select(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value
                                ],
                                $arResult[ 'FIELD_VALUES_CURRENCY' ]);
                            break;
                        case 'enumeration':
                            if(!empty($arField[ 'items' ]))
                                $arList = array_column($arField[ 'items' ], 'VALUE', 'ID');
        
                            $return = CPrintForm ::select(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value
                                ],
                                $arList);
                            break;
                        case 'crm_multifield'://its simple example: need multifield, check data type and more...
                            if(!empty($value) && is_array($value))
                                $value = reset($value)[ 'VALUE' ];
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => false,
                                    'VALUE' => $value,
                                    'TYPE' => 'text',
                                ]);
                            break;
                        case 'crm_company':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'text',
                                ]);
                            if(!empty($arResult[ 'VALUE_COMPANY_ID' ]) && $value == $arResult[ 'VALUE_COMPANY_ID' ][ 'ID' ])
                            {
                                $return .= '(' . $arResult[ 'VALUE_COMPANY_ID' ][ 'TITLE' ] . ')';
                            }
                            break;
                        case 'crm_contact':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'text',
                                ]);
        
                            if(!empty($arResult[ 'VALUE_CONTACT_ID' ]) && $value == $arResult[ 'VALUE_CONTACT_ID' ][ 'ID' ])
                            {
                                $return .= '(' . implode(' ', [$arResult[ 'VALUE_CONTACT_ID' ][ 'NAME' ], $arResult[ 'VALUE_CONTACT_ID' ][ 'LAST_NAME' ]]) . ')';
                            }
                            break;
                        case 'file':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'file',
                                ]);
                            if($arField[ 'isMultiple' ])
                            {
                                if(is_array($value))
                                {
                                    foreach($value as $k => $val)
                                    {
                                        if(!empty($val[ 'downloadUrl' ]))
                                        {
                                            $return .= '<br/><a href="' . $val[ 'downloadUrl' ] . '">old file ' . $k . '</a>';
                                        }
                                    }
                                }
                            }
                            else
                            {
                                if(!empty($value[ 'downloadUrl' ]))
                                {
                                    $return .= '<br/><a href="' . $value[ 'downloadUrl' ] . '">old file</a>';
                                }
                            }
                            break;
                        case 'date':
                            if(!empty($value))
                            {
                                $value = date('Y-m-d', strtotime($value));
                            }
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'date',
                                ]);
                            break;
                        case 'datetime':
                            if(!empty($value))
                            {
                                $value = date('Y-m-d\TH:i:s', strtotime($value));
                            }
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'datetime-local',
                                ]);
                            break;
                        case 'char':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => 'Y',
                                    'CHECKED' => ($value == 'Y') ? true : false,
                                    'TYPE' => 'checkbox',
                                ]);
                            break;
        
                        case 'boolean':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => '1',
                                    'CHECKED' => ($value == 'Y') ? true : false,
                                    'TYPE' => 'checkbox',
                                ]);
                            break;
                        case 'double':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'number'
                                ]);
                            break;
                        case 'url':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'text',
                                ]);
                            break;
                        case 'integer':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'number',
                                ]);
                            break;
                        case 'user':
                            $arUserNames = [];
                            if(!empty($value))
                            {
                                foreach($sb->getUserScope()->user()->get([], ['ID' => $value], true)->getUsers() as $user)
                                {
                                    $arUserNames[] = implode(' ', [$user->NAME, $user->LAST_NAME]);
                                }
                            }
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'number'
                                ]);
                            if(!empty($arUserNames))
                            {
                                $return .= '(' . implode(', ', $arUserNames) . ')';
                            }
        
                            break;
                        case 'money':
                            list($money, $currency) = explode('|', $value);
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $money,
                                    'TYPE' => 'number',
                                ]);
                            $return .= CPrintForm ::select(
                                [
                                    'NAME' => $key . '_CURRENCY',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $currency
                                ],
                                $arResult[ 'FIELD_VALUES_CURRENCY' ]);
                            break;
                        case 'address':
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'text',
                                ]);
                            break;
                        case 'resourcebooking':
                            //some code booking
                            $return = 'field not support';
                            break;
                        default:
        
                            $return = CPrintForm ::input(
                                [
                                    'NAME' => 'form[' . $key . ']',
                                    'REQUIRED' => $arField[ 'isRequired' ],
                                    'DISABLE' => $arField[ 'isReadOnly' ],
                                    'MULTIPLE' => $arField[ 'isMultiple' ],
                                    'VALUE' => $value,
                                    'TYPE' => 'text',
                                ]);
        
                            break;
                    }
        
                    if(strpos($key, 'UF_') === 0)
                    {
                        $sResultCustom .= '<div class="col-4 mt-3">' . (($arField[ 'formLabel' ]) ? $arField[ 'formLabel' ] : $arField[ 'title' ]) . ': ' . '</div>';
                        $sResultCustom .= '<div class="col-6 mt-3">' . $return . '</div>';
                    }
                    else
                    {
                        $sResult .= '<div class="col-4 mt-3">' . (($arField[ 'formLabel' ]) ? $arField[ 'formLabel' ] : $arField[ 'title' ]) . ': ' . '</div>';
                        $sResult .= '<div class="col-6 mt-3">' . $return . '</div>';
                    }
                }
        
        ?>
        	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"  crossorigin="anonymous">
        	<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"></script>
        	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script>
        	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"  crossorigin="anonymous"></script>
        	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        	<script>
        		$(document).ready(function()
            {
                    $('#auto_form').on( 'submit', function(el) {//event submit form
                        el.preventDefault();//the default action of the event will not be triggered
                        var formData = new FormData(this);
                        var xhr = new XMLHttpRequest();
                        xhr.open("POST", 'auto_form.php');
                        xhr.onreadystatechange = function () {
                            if (this.readyState === 4) {
                                if (this.status >= 200 && this.status < 400) {
                                    // Success!
                                    var resp = this.responseText;
                                    try {
                                        var json = JSON.parse(resp);
                                        if (typeof json.message !== 'undefined') {
                                            alert(json.message);
                                        }
                                    } catch (e) {
                                        return false;
                                    }
                                } else {
                                    alert('error');
                                }
                            }
                        };
                        xhr.send(formData);
                    });
                });
            </script>
            <div class="container">
                <form id="auto_form" action="" enctype="multipart/form-data" method="post">
                    <?php if(!empty($arResult[ 'ITEM' ][ 'ID' ]))://for update entity ?>
                        <input type="hidden" name="form[ID]" value="<?= $arResult[ 'ITEM' ][ 'ID' ] ?>">
                    <?php endif; ?>
                    <h2>Standard fields</h2>
                    <div class="row">
                        <?= $sResult ?>
                    </div>
                    <h2>Custom fields</h2>
                    <div class="row">
                        <?= $sResultCustom ?>
                    </div>
                    <div class="row">
                        <div class="col-sm-10 mt-5">
                            <input type="submit" class="btn btn-primary" value="Submit">
                        </div>
                    </div>
                </form>
            </div>
        <?php endif; ?>
        
# pip install b24pysdk flask
        from flask import Flask, request
        from markupsafe import escape
        from b24pysdk import BitrixWebhook, Client
        
        app = Flask(__name__)
        
        client = Client(BitrixWebhook(
            domain="your-domain.bitrix24.com",
            webhook_token="USER_ID/TOKEN",  # user_id/token only, without https://
        ))
        
        def input_field(params):
            """Builds <input> (or a set for a multiple field)."""
            count = 2 if params.get("MULTIPLE") and params.get("TYPE") != "file" else 0
            values = params.get("VALUE") if isinstance(params.get("VALUE"), list) else []
            html = ""
            for i in range(count + 1):
                value = (values[i] if i < len(values) else "") if count > 0 else params.get("VALUE")
                html += '<input class="form-control' + ("-file" if params.get("TYPE") == "file" else "") + '"'
                if params.get("NAME"):
                    html += f' name="{params["NAME"]}{"[]" if params.get("MULTIPLE") else ""}"'
                if params.get("TYPE"):
                    html += f' type="{params["TYPE"]}"'
                if params.get("REQUIRED"):
                    html += " required"
                if params.get("DISABLE"):
                    html += " disabled"
                if params.get("CHECKED"):
                    html += " checked"
                if params.get("MULTIPLE"):
                    html += " multiple"
                if value:
                    html += f' value="{value}"'
                html += ">"
            return html
        
        def select_field(params, options):
            """Builds <select> from a \"value => label\" dictionary."""
            if not options:
                return ""
            html = '<select class="form-control"'
            if params.get("NAME"):
                html += f' name="{params["NAME"]}{"[]" if params.get("MULTIPLE") else ""}"'
            if params.get("REQUIRED"):
                html += " required"
            if params.get("DISABLE"):
                html += " disabled"
            if params.get("MULTIPLE"):
                html += " multiple"
            html += ">"
            value = params.get("VALUE")
            value = [str(v) for v in value] if isinstance(value, list) else [str(value or "")]
            for key, title in options.items():
                selected = " selected" if str(key) in value else ""
                html += f'<option value="{key}"{selected}>{title}</option>'
            return html + "</select>"
        
        PAGE = """
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
            <div class="container">
                <form id="auto_form" enctype="multipart/form-data" method="post">
                    %(hidden_id)s
                    <h2>Standard fields</h2>
                    <div class="row">%(standard)s</div>
                    <h2>Custom fields</h2>
                    <div class="row">%(custom)s</div>
                    <div class="row"><div class="col-sm-10 mt-5">
                        <input type="submit" class="btn btn-primary" value="Submit">
                    </div></div>
                </form>
            </div>
            <script>
                document.getElementById('auto_form').addEventListener('submit', async (el) => {
                    el.preventDefault();
                    const response = await fetch('/form', { method: 'POST', body: new FormData(el.currentTarget) });
                    const json = await response.json();
                    if (json.message) alert(json.message);
                });
            </script>
        """
        
        @app.route("/")
        def form_page():
            item_id = int(request.args.get("ID", 0) or 0)
        
            # Field descriptions and standard list values (sequential calls)
            ar_result = {}
            ar_result["FIELDS"] = client.crm.lead.fields().result
            ar_result["FIELD_VALUES_SOURCE_ID"] = client.crm.status.list(
                filter={"ENTITY_ID": ar_result["FIELDS"]["SOURCE_ID"]["statusType"]}).result
            ar_result["FIELD_VALUES_STATUS_ID"] = client.crm.status.list(
                filter={"ENTITY_ID": ar_result["FIELDS"]["STATUS_ID"]["statusType"]}).result
            ar_result["FIELD_VALUES_CURRENCY"] = client.crm.currency.list().result
        
            if item_id > 0:
                # We get the element and values of related objects if it is an edit form
                ar_result["ITEM"] = client.crm.lead.get(bitrix_id=item_id).result
                if ar_result["ITEM"].get("COMPANY_ID") and ar_result["ITEM"]["COMPANY_ID"] != "0":
                    ar_result["VALUE_COMPANY_ID"] = client.crm.company.get(
                        bitrix_id=int(ar_result["ITEM"]["COMPANY_ID"])).result
                if ar_result["ITEM"].get("CONTACT_ID") and ar_result["ITEM"]["CONTACT_ID"] != "0":
                    ar_result["VALUE_CONTACT_ID"] = client.crm.contact.get(
                        bitrix_id=int(ar_result["ITEM"]["CONTACT_ID"])).result
        
            item = ar_result.get("ITEM", {})
            s_result = ""
            s_result_custom = ""
            for key, field in ar_result["FIELDS"].items():
                value = item.get(key) or ""
                params = {"NAME": f"form[{key}]", "REQUIRED": field.get("isRequired"),
                          "DISABLE": field.get("isReadOnly"), "MULTIPLE": field.get("isMultiple"), "VALUE": value}
                ret = ""
                field_type = field.get("type")
                if field_type == "crm_status":
                    rows = ar_result.get("FIELD_VALUES_" + key) or client.crm.status.list(
                        filter={"ENTITY_ID": field["statusType"]}).result
                    ret = select_field(params, {r["STATUS_ID"]: r["NAME"] for r in rows})
                elif field_type == "crm_currency":
                    options = {c["CURRENCY"]: c["FULL_NAME"] for c in ar_result["FIELD_VALUES_CURRENCY"]}
                    ret = select_field(params, options)
                elif field_type == "enumeration":
                    options = {it["ID"]: it["VALUE"] for it in field.get("items", [])}
                    ret = select_field(params, options)
                elif field_type == "crm_multifield":
                    if isinstance(value, list) and value:
                        value = value[0]["VALUE"]
                    ret = input_field({**params, "MULTIPLE": False, "VALUE": value, "TYPE": "text"})
                elif field_type == "crm_company":
                    ret = input_field({**params, "TYPE": "text"})
                    company = ar_result.get("VALUE_COMPANY_ID")
                    if company and str(value) == str(company["ID"]):
                        ret += f"({company['TITLE']})"
                elif field_type == "crm_contact":
                    ret = input_field({**params, "TYPE": "text"})
                    contact = ar_result.get("VALUE_CONTACT_ID")
                    if contact and str(value) == str(contact["ID"]):
                        ret += f"({contact['NAME']} {contact['LAST_NAME']})"
                elif field_type == "date":
                    if value:
                        value = str(value)[:10]
                    ret = input_field({**params, "VALUE": value, "TYPE": "date"})
                elif field_type == "datetime":
                    if value:
                        value = str(value)[:19]
                    ret = input_field({**params, "VALUE": value, "TYPE": "datetime-local"})
                elif field_type == "char":
                    ret = input_field({**params, "VALUE": "Y", "CHECKED": value == "Y", "TYPE": "checkbox"})
                elif field_type == "boolean":
                    ret = input_field({**params, "VALUE": "1", "CHECKED": value == "Y", "TYPE": "checkbox"})
                elif field_type in ("double", "integer"):
                    ret = input_field({**params, "TYPE": "number"})
                elif field_type == "user":
                    ret = input_field({**params, "TYPE": "number"})
                    if value:
                        users = client.user.get(filter={"ID": value}).result
                        if users:
                            ret += "(" + ", ".join(f"{u['NAME']} {u['LAST_NAME']}" for u in users) + ")"
                elif field_type == "money":
                    money, _, currency = str(value).partition("|")
                    ret = input_field({**params, "VALUE": money, "TYPE": "number"})
                    options = {c["CURRENCY"]: c["FULL_NAME"] for c in ar_result["FIELD_VALUES_CURRENCY"]}
                    ret += select_field({"NAME": f"{key}_CURRENCY", "REQUIRED": field.get("isRequired"),
                                         "DISABLE": field.get("isReadOnly"), "MULTIPLE": field.get("isMultiple"),
                                         "VALUE": currency}, options)
                elif field_type == "resourcebooking":
                    ret = "field not support"
                else:
                    ret = input_field({**params, "TYPE": "text"})
        
                label = field.get("formLabel") or field.get("title")
                block = f'<div class="col-4 mt-3">{label}: </div><div class="col-6 mt-3">{ret}</div>'
                if key.startswith("UF_"):
                    s_result_custom += block
                else:
                    s_result += block
        
            # A hidden field with ID is added only for the edit form
            hidden_id = ""
            if item.get("ID"):
                hidden_id = f'<input type="hidden" name="form[ID]" value="{escape(item["ID"])}">'
            return PAGE % {"hidden_id": hidden_id, "standard": s_result, "custom": s_result_custom}
        
        if __name__ == "__main__":
            app.run(port=5000)
        

Handler code that saves the form:

import { B24Hook } from '@bitrix24/b24jssdk'
        
        const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
        // B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
        
        // Handler: saves the form — updates an existing lead or creates a new one.
        // req.body.form — an object with form fields (Express with express.urlencoded()).
        export async function handler(req, res) {
            const arForm = { ...req.body.form }
        
            // Convert values to REST format based on field type
            const arFields = (await $b24.actions.v2.call.make({
                method: 'crm.lead.fields', params: {}, requestId: 'lead-fields-save',
            })).getData().result
            for (const [key, prop] of Object.entries(arFields)) {
                if (!(key in arForm)) {
                    // Fields not present in the form are cleared (except read-only and files)
                    if (!prop.isReadOnly && prop.type !== 'file') {
                        if (prop.type === 'enumeration' && prop.isMultiple) arForm[key] = [false]
                        else if (prop.isMultiple) arForm[key] = []
                        else arForm[key] = ''
                    }
                    continue
                }
                if (prop.type === 'crm_multifield') {
                    arForm[key] = [{ VALUE: arForm[key] }]
                } else if (prop.type === 'money') {
                    arForm[key] = [arForm[key], arForm[`${key}_CURRENCY`]].join('|')
                    delete arForm[`${key}_CURRENCY`]
                }
            }
        
            const id = parseInt(String(arForm.ID ?? '0'), 10) || 0
            delete arForm.ID
            if (id > 0) {
                const result = await $b24.actions.v2.call.make({
                    method: 'crm.lead.update', params: { id, fields: arForm }, requestId: 'lead-update',
                })
                res.json({ message: result.getData().result ? 'Lead update' : 'Lead not updated' })
            } else {
                const result = await $b24.actions.v2.call.make({
                    method: 'crm.lead.add', params: { fields: arForm }, requestId: 'lead-add',
                })
                const newId = result.getData().result
                res.json({ message: newId ? `Lead add ID:${newId}` : 'Lead not added' })
            }
        }
        
<?php
            // composer require bitrix24/b24phpsdk:"^3.0"
            require_once 'vendor/autoload.php';
        
            use Bitrix24\SDK\Services\ServiceBuilderFactory;
            use Symfony\Component\EventDispatcher\EventDispatcher;
            use Psr\Log\NullLogger;
        
            $sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
                ->initFromWebhook('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/');
            $crm = $sb->getCRMScope();
        
            $arForm = [];
            foreach($_POST['form'] as $key => $item)
            {
                if(is_array($item))
                {
                    $arForm[$key] = [];
                    foreach($item as $k => $val)
                    {
                        $arForm[$key][$k] = htmlspecialchars($val);
                    }
                }
                else
                {
                    $arForm[$key] = htmlspecialchars($item);
                }
            }
            //make array multiple files for add to custom field
            if(!empty($_FILES['form']['tmp_name']) && is_array($_FILES['form']['tmp_name']))
            {
                foreach($_FILES['form']['tmp_name'] as $key => $files)
                {
                    if(is_array($files))
                    {
                        foreach($files as $k => $file)
                        {
                            $arForm[$key][$k] = [
                                "fileData" => [
                                    $_FILES['form']['name'][$key][$k],
                                    base64_encode(file_get_contents($file))
                                ]
                            ];
                        }
                    }
                    else
                    {
                        $arForm[$key] = [
                            "fileData" => [
                                $_FILES['form']['name'][$key],
                                base64_encode(file_get_contents($files))
                            ]
                        ];
                    }
                }
            }
            $arFields = $crm->lead()->fields()->getFieldsDescription();
            if(!empty($arFields))
            {
                foreach($arFields as $key => $prop)
                {
                    if(!isset($arForm[$key]))
                    {
                        if(!$prop['isReadOnly'] && $prop['type'] != 'file')
                        {
                            if($prop['type'] == 'enumeration' && $prop['isMultiple'])
                            {
                                //if type multiple enumeration to clean selected value need send: [false]
                                $arForm[$key] = [false];
                            }
                            elseif($prop['isMultiple'])
                            {
                                $arForm[$key] = [];
                            }
                            else
                            {
                                $arForm[$key] = '';
                            }
                        }
                        continue;
                    }
                    //here may be any check field example by type
                    if($prop['type'] == 'crm_multifield')
                    {
                        if(isset($arForm[$key]))
                        {
                            $arForm[$key] = [['VALUE' => $arForm[$key]]];
                        }
                    }
                    elseif($prop['type'] == 'money')
                    {
                        $arForm[$key] = implode('|', [$arForm[$key], $arForm[$key . '_CURRENCY']]);
                        unset($arForm[$key . '_CURRENCY']);
                    }
                }
            }
            $arForm['ID'] = intVal($arForm['ID']);
            if($arForm['ID'] > 0)
            {
                $id = $arForm['ID'];
                unset($arForm['ID']);
                $result = $crm->lead()->update($id, $arForm)->isSuccess();
                $arMess = [
                    'success' => 'Lead update',
                    'error' => 'Lead not updated',
                ];
                echo json_encode(['message' => $result ? $arMess['success'] : $arMess['error']]);
            }
            else
            {
                unset($arForm['ID']);
                $resultId = $crm->lead()->add($arForm)->getId();
                $arMess = [
                    'success' => 'Lead add',
                    'error' => 'Lead not added',
                ];
                echo json_encode(['message' => $resultId ? $arMess['success'] . ' ID:' . $resultId : $arMess['error']]);
            }
        
# pip install b24pysdk flask
        from flask import Flask, request, jsonify
        from b24pysdk import BitrixWebhook, Client
        
        app = Flask(__name__)
        
        client = Client(BitrixWebhook(
            domain="your-domain.bitrix24.com",
            webhook_token="USER_ID/TOKEN",  # user_id/token only, without https://
        ))
        
        @app.route("/form", methods=["POST"])
        def save_form():
            # request.form contains keys in the form of form[FIELD]; collect into a nested dictionary
            ar_form = {}
            for full_key in request.form:
                if full_key.startswith("form[") and full_key.endswith("]"):
                    ar_form[full_key[5:-1]] = request.form[full_key]
        
            # Convert values to REST format based on field type
            ar_fields = client.crm.lead.fields().result
            for key, prop in ar_fields.items():
                if key not in ar_form:
                    # Fields not present in the form are cleared (except read-only and files)
                    if not prop.get("isReadOnly") and prop.get("type") != "file":
                        if prop.get("type") == "enumeration" and prop.get("isMultiple"):
                            ar_form[key] = [False]
                        elif prop.get("isMultiple"):
                            ar_form[key] = []
                        else:
                            ar_form[key] = ""
                    continue
                if prop.get("type") == "crm_multifield":
                    ar_form[key] = [{"VALUE": ar_form[key]}]
                elif prop.get("type") == "money":
                    ar_form[key] = "|".join([ar_form[key], ar_form.get(f"{key}_CURRENCY", "")])
                    ar_form.pop(f"{key}_CURRENCY", None)
        
            item_id = int(ar_form.get("ID") or 0)
            ar_form.pop("ID", None)
            if item_id > 0:
                result = client.crm.lead.update(bitrix_id=item_id, fields=ar_form).result
                message = "Lead update" if result else "Lead not updated"
            else:
                new_id = client.crm.lead.add(fields=ar_form).result
                message = f"Lead add ID:{new_id}" if new_id else "Lead not added"
            return jsonify(message=message)
        
        if __name__ == "__main__":
            app.run(port=5000)