Добавить лид через веб-форму
Scope:
crmКто может выполнять методы: чтобы пройти сценарий целиком, нужны оба права — на добавление лидов и на чтение лидов
- crm.item.add — пользователь с правом на добавление лидов
- crm.item.get — пользователь с правом на чтение лидов
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
На сайте можно разместить форму для сбора данных потенциальных клиентов. Когда клиент заполнит форму, обработчик на вашем сервере создаст лид в CRM и вернет его идентификатор. В результате у вас будет два файла — страница с формой и обработчик на выбранном стеке — и отдельный скрипт для проверки результата.
Сценарий состоит из двух шагов.
-
Разместить форму на HTML-странице. Форма отправит данные в обработчик
-
Создать обработчик. Он проверит данные и создаст лид универсальным методом crm.item.add
После этого проверим результат методом crm.item.get по идентификатору из ответа второго шага: этот вызов только подтверждает создание и данные в CRM не меняет.
Подготовим данные
Для выполнения примера нужны:
-
входящий вебхук со scope
crm. Обработчик работает на сервере: страница с формой вебхук не использует -
права у пользователя, от имени которого создан вебхук: на добавление лидов — для шага 2, на чтение лидов — для шага проверки
-
классический режим CRM. В простом режиме лидов в CRM нет: при создании лида с заполненным именем система автоматически конвертирует его в сделку, а поле
STATUS_IDпринимает значениеCONVERTED. Текущий режим возвращает метод crm.settings.mode.get, а сценарий для обоих режимов разобран в туториале Добавить дело в новый лид или сделку в зависимости от режима CRM -
страница с формой и обработчик на одном домене и порте. В примере
handlerUrl— относительный путь, поэтому запрос уходит на тот адрес, с которого открыта страница. Если открытьform.htmlс другого адреса или из файловой системы, обработчик данные не получит -
переменные окружения с данными вебхука. Задайте их в окружении процесса перед запуском, в код не вписывайте:
-
B24_HOOK— полный URL вебхука, для JS и PHP -
B24_DOMAINиB24_WEBHOOK_TOKEN— домен иUSER_ID/TOKENбезhttps://, для Python
Например, при локальном запуске:
-
Node.js —
B24_HOOK='https://your-domain.bitrix24.ru/rest/1/TOKEN/' node handler.mjs -
PHP —
B24_HOOK='https://your-domain.bitrix24.ru/rest/1/TOKEN/' php -S localhost:3000 -t public -
Python —
B24_DOMAIN='your-domain.bitrix24.ru' B24_WEBHOOK_TOKEN='1/TOKEN' python handler.py
-
Если в вашем Битрикс24 для лидов настроены обязательные поля, их тоже нужно передать в fields метода crm.item.add — иначе метод вернет ошибку. Список полей с признаком isRequired вернет метод crm.item.fields с entityTypeId со значением 1.
Для серверных JS-примеров с B24Hook нужен Node.js 18, 20, 22 или новее. Для новых проектов берите 22 или новее: поддержка Node.js 18 и 20 сообществом завершена. B24JsSDK — ES module: сохраните код в файле .mjs или добавьте "type": "module" в package.json.
Для примеров с b24pysdk нужен Python 3.9 или новее.
Для примеров с bitrix24/b24phpsdk:"^3.3" нужен PHP 8.4 или новее с расширениями curl, intl и json, а для проверки длины значений в примере — mbstring. Требования SDK и рекомендованную раскладку файлов смотрите на странице B24PhpSDK.
1. Создаем веб-форму
В Битрикс24 из лида можно автоматически создать контакт и компанию. Чтобы форма подходила для разных случаев, сделаем ее универсальной. Для контакта нужно указать имя и фамилию, а для компании — название. Создадим на странице сайта веб-форму с пятью полями:
-
NAME— имя, обязательное поле формы -
LAST_NAME— фамилия -
COMPANY_TITLE— название компании -
EMAIL— электронная почта -
PHONE— телефон
Сохраните страницу в файл form.html. Где он должен лежать, зависит от обработчика:
-
Node.js — в подпапке
publicтого каталога, где лежит файл обработчика. Запускайтеnodeиз этого каталога:express.staticищет папкуpublicотносительно рабочего каталога процесса. Страница откроется по адресуhttp://localhost:3000/form.html -
Flask — в папке
static. Страница откроется по адресуhttp://localhost:3000/static/form.html -
PHP — в публичном каталоге веб-сервера вместе с
form.php. Каталогvendorдержите выше публичного, чтобы он не был доступен из браузера. При локальном запускеphp -S localhost:3000 -t publicстраница откроется по адресуhttp://localhost:3000/form.html
Адреса заработают после запуска из шага 2: в Node.js и Flask страницу отдает то же приложение, что принимает данные формы, в PHP — веб-сервер, на котором лежит form.php.
Форма отправляет данные в обработчик методом POST в формате application/x-www-form-urlencoded. Один и тот же код работает со всеми тремя вариантами обработчика: меняется только адрес в переменной handlerUrl.
<form id="form_to_crm">
<input type="text" name="NAME" placeholder="Имя" required>
<input type="text" name="LAST_NAME" placeholder="Фамилия">
<input type="text" name="COMPANY_TITLE" placeholder="Название компании">
<input type="text" name="EMAIL" placeholder="Почта">
<input type="text" name="PHONE" placeholder="Телефон">
<input type="submit" value="Отправить">
</form>
<script>
// Адрес обработчика: '/form' — для Node.js и Flask, 'form.php' — для PHP
const handlerUrl = '/form';
document.getElementById('form_to_crm').addEventListener('submit', async (event) => {
event.preventDefault(); // Отменяем стандартную отправку формы
// Собираем поля формы в тело запроса
const body = new URLSearchParams(new FormData(event.currentTarget));
const response = await fetch(handlerUrl, { method: 'POST', body });
// Обработчик отвечает JSON. Если пришло что-то другое, показываем общее сообщение
const data = await response.json().catch(() => ({ message: 'Сервер вернул неожиданный ответ' }));
// Показываем результат: идентификатор понадобится для проверки через REST
alert(data.id ? data.message + '. ID: ' + data.id : data.message);
});
</script>
Атрибут required проверяет обязательное поле только в браузере. Запрос можно отправить в обход формы, поэтому обработчик проверяет данные повторно.
2. Создаем обработчик формы
Обработчик принимает данные формы, проверяет их и добавляет лид методом crm.item.add. В параметре entityTypeId передаем 1 — тип объекта «Лид», значения для остальных типов приведены в справочнике типов объектов CRM. В объекте fields передаем поля:
-
title— название лида. Составляем его из имени, фамилии и названия компании -
name— имя -
lastName— фамилия -
companyTitle— название компании -
fm— массив мультиполей, в нем передаем телефон и электронную почту
Универсальные методы crm.item.* используют имена полей в camelCase. Они отличаются от имен в методах отдельных объектов: title вместо TITLE, lastName вместо LAST_NAME, а телефон и почта передаются одним массивом fm вместо отдельных полей PHONE и EMAIL.
Значения полей получаем из формы. Ниже разобрано каждое действие обработчика, полный код приведен в конце раздела.
Как использовать примеры в документации
Принимаем запрос и подключаем SDK
Обработчик принимает POST-запрос по адресу, который указан в переменной handlerUrl на странице с формой. С Битрикс24 работаем через входящий вебхук.
// npm install express @bitrix24/b24jssdk
import express from 'express'
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
const app = express()
// Форма отправляет данные в формате application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }))
// Отдаем страницу с формой из папки public
app.use(express.static('public'))
// Шаблон для проверки адреса электронной почты
const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
// Ограничение длины значений: форма публичная
const maxLength = 100
// Обработчик принимает данные формы по маршруту /form
app.post('/form', async (req, res) => {
// Тело обработчика — в следующих шагах
})
// Запуск: node handler.mjs
app.listen(3000)
<?php
// composer require bitrix24/b24phpsdk:"^3.3"
// form.php и form.html лежат в публичном каталоге, vendor — выше него
// Локальный запуск: php -S localhost:3000 -t public
require_once __DIR__ . '/../vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
header('Content-Type: application/json; charset=utf-8');
// Ограничение длины значений: форма публичная
const MAX_LENGTH = 100;
// Обработчик принимает только POST-запросы
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['message' => 'Метод не поддерживается']);
exit;
}
$sb = ServiceBuilderFactory::createServiceBuilderFromWebhook(getenv('B24_HOOK'));
# pip install flask b24pysdk
import os
import re
from flask import Flask, request, jsonify
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError, BitrixSDKException
# Страницу form.html кладем в папку static
app = Flask(__name__)
client = Client(BitrixWebhook(
domain=os.environ["B24_DOMAIN"], # your-domain.bitrix24.ru
webhook_token=os.environ["B24_WEBHOOK_TOKEN"], # только user_id/token, без https://
))
# Шаблон для проверки адреса электронной почты
EMAIL_PATTERN = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+")
# Ограничение длины значений: форма публичная
MAX_LENGTH = 100
@app.route("/form", methods=["POST"])
def handle_form():
... # Тело обработчика — в следующих шагах
# Запуск: python handler.py
if __name__ == "__main__":
app.run(port=3000)
Проверяем данные из формы
Данные приходят от анонимного посетителя, поэтому до вызова метода обработчик проверяет их:
-
обращается к полям со значением по умолчанию: нужного ключа может не быть в запросе
-
убирает пробелы по краям и не создает лид, если не заполнено имя
-
проверяет формат электронной почты, чтобы не сохранить в CRM заведомо неверный адрес
-
отклоняет слишком длинные значения: в публичную форму может прийти что угодно
В REST значения передаем в исходном виде. Не применяйте к ним htmlspecialchars и другие функции HTML-экранирования: они нужны при выводе данных на страницу, а в CRM из-за них вместо Иванов & сын попадет Иванов & сын.
// Получаем данные из формы
const sName = String(req.body.NAME ?? '').trim()
const sLastName = String(req.body.LAST_NAME ?? '').trim()
const sCompanyTitle = String(req.body.COMPANY_TITLE ?? '').trim()
const sPhone = String(req.body.PHONE ?? '').trim()
const sEmail = String(req.body.EMAIL ?? '').trim()
// Проверяем данные до вызова метода
if (!sName) {
res.status(400).json({ message: 'Заполните имя' })
return
}
if (sEmail && !emailPattern.test(sEmail)) {
res.status(400).json({ message: 'Проверьте адрес электронной почты' })
return
}
if ([sName, sLastName, sCompanyTitle, sPhone, sEmail].some(value => value.length > maxLength)) {
res.status(400).json({ message: 'Одно из полей слишком длинное' })
return
}
// Получаем данные из формы
$sName = trim((string)($_POST['NAME'] ?? ''));
$sLastName = trim((string)($_POST['LAST_NAME'] ?? ''));
$sCompanyTitle = trim((string)($_POST['COMPANY_TITLE'] ?? ''));
$sPhone = trim((string)($_POST['PHONE'] ?? ''));
$sEmail = trim((string)($_POST['EMAIL'] ?? ''));
// Проверяем данные до вызова метода
if ($sName === '') {
http_response_code(400);
echo json_encode(['message' => 'Заполните имя']);
exit;
}
if ($sEmail !== '' && !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/u', $sEmail)) {
http_response_code(400);
echo json_encode(['message' => 'Проверьте адрес электронной почты']);
exit;
}
foreach ([$sName, $sLastName, $sCompanyTitle, $sPhone, $sEmail] as $value) {
if (mb_strlen($value) > MAX_LENGTH) {
http_response_code(400);
echo json_encode(['message' => 'Одно из полей слишком длинное']);
exit;
}
}
# Получаем данные из формы
s_name = request.form.get("NAME", "").strip()
s_last_name = request.form.get("LAST_NAME", "").strip()
s_company_title = request.form.get("COMPANY_TITLE", "").strip()
s_phone = request.form.get("PHONE", "").strip()
s_email = request.form.get("EMAIL", "").strip()
# Проверяем данные до вызова метода
if not s_name:
return jsonify({"message": "Заполните имя"}), 400
if s_email and not EMAIL_PATTERN.fullmatch(s_email):
return jsonify({"message": "Проверьте адрес электронной почты"}), 400
if any(len(value) > MAX_LENGTH for value in (s_name, s_last_name, s_company_title, s_phone, s_email)):
return jsonify({"message": "Одно из полей слишком длинное"}), 400
Собираем телефон и почту в мультиполя
Телефон и электронную почту метод принимает в поле fm — это массив объектов crm_multifield. У каждого объекта три ключа:
-
typeId— тип мультиполя:PHONEдля телефона,EMAILдля почты -
valueType— тип значения, напримерWORK— рабочий,HOME— домашний -
value— значение из формы
Если посетитель не заполнил поле, объект в массив не добавляем. Если не заполнено ни одно значение, передаем пустой массив.
В справочнике crm_multifield ключи мультиполя приведены в верхнем регистре — TYPE_ID, VALUE_TYPE, VALUE. Это формат методов отдельных объектов, например crm.lead.add. Универсальные методы crm.item.* принимают и возвращают те же ключи в camelCase.
// Собираем телефон и почту в мультиполя
const arFm = []
if (sPhone) {
arFm.push({ typeId: 'PHONE', valueType: 'WORK', value: sPhone })
}
if (sEmail) {
arFm.push({ typeId: 'EMAIL', valueType: 'HOME', value: sEmail })
}
// Собираем телефон и почту в мультиполя
$arFm = [];
if ($sPhone !== '') {
$arFm[] = ['typeId' => 'PHONE', 'valueType' => 'WORK', 'value' => $sPhone];
}
if ($sEmail !== '') {
$arFm[] = ['typeId' => 'EMAIL', 'valueType' => 'HOME', 'value' => $sEmail];
}
# Собираем телефон и почту в мультиполя
ar_fm = []
if s_phone:
ar_fm.append({"typeId": "PHONE", "valueType": "WORK", "value": s_phone})
if s_email:
ar_fm.append({"typeId": "EMAIL", "valueType": "HOME", "value": s_email})
Формируем название лида
Название собираем из имени и фамилии. Если посетитель указал название компании, добавляем его через тире — так менеджер увидит в списке лидов, от кого пришла заявка.
// Формируем название лида из имени и фамилии
let sTitle = 'С сайта: ' + `${sName} ${sLastName}`.trim()
// Если есть название компании — добавляем его через тире после имени и фамилии
if (sCompanyTitle) {
sTitle += ' — ' + sCompanyTitle
}
// Формируем название лида из имени и фамилии
$sTitle = 'С сайта: ' . trim($sName . ' ' . $sLastName);
// Если есть название компании — добавляем его через тире после имени и фамилии
if ($sCompanyTitle !== '') {
$sTitle .= ' — ' . $sCompanyTitle;
}
# Формируем название лида из имени и фамилии
s_title = "С сайта: " + f"{s_name} {s_last_name}".strip()
# Если есть название компании — добавляем его через тире после имени и фамилии
if s_company_title:
s_title += " — " + s_company_title
Создаем лид
Подготовленные значения передаем в fields метода crm.item.add. Обработчик возвращает странице идентификатор созданного лида в поле id. Текст ошибки пишем в лог сервера, а посетителю возвращаем общее сообщение: так технические подробности не попадут на публичную страницу.
// Отправляем данные в Битрикс24
try {
const response = await $b24.actions.v2.call.make({
method: 'crm.item.add',
params: {
entityTypeId: 1, // Тип объекта CRM — лид
fields: {
title: sTitle, // Название лида
name: sName, // Имя
lastName: sLastName, // Фамилия
companyTitle: sCompanyTitle, // Название компании
fm: arFm, // Телефон и почта
}
},
requestId: 'lead-add'
})
// Проверяем результат и выводим сообщение
if (!response.isSuccess) {
// Подробности ошибки пишем в лог, посетителю их не показываем
console.error(response.getErrorMessages().join('; '))
res.status(502).json({ message: 'Не удалось создать лид, попробуйте позже' })
return
}
const leadId = response.getData().result.item.id // Идентификатор созданного лида
console.info('Создан лид с ID ' + leadId)
res.json({ message: 'Лид создан', id: leadId })
} catch (error) {
// Сетевые ошибки и сбои SDK приходят исключением
console.error(error)
res.status(502).json({ message: 'Не удалось создать лид, попробуйте позже' })
}
// Отправляем данные в Битрикс24
try {
$result = $sb->getCRMScope()->item()->add(1, [ // 1 — тип объекта CRM «Лид»
'title' => $sTitle, // Название лида
'name' => $sName, // Имя
'lastName' => $sLastName, // Фамилия
'companyTitle' => $sCompanyTitle, // Название компании
'fm' => $arFm, // Телефон и почта
]);
$leadId = $result->item()->id; // Идентификатор созданного лида
error_log('Создан лид с ID ' . $leadId);
echo json_encode(['message' => 'Лид создан', 'id' => $leadId]);
} catch (\Throwable $e) {
// Подробности ошибки пишем в лог, посетителю их не показываем
error_log($e->getMessage());
http_response_code(502);
echo json_encode(['message' => 'Не удалось создать лид, попробуйте позже']);
}
# Отправляем данные в Битрикс24
try:
bitrix_response = client.crm.item.add(
entity_type_id=1, # Тип объекта CRM — лид
fields={
"title": s_title, # Название лида
"name": s_name, # Имя
"lastName": s_last_name, # Фамилия
"companyTitle": s_company_title, # Название компании
"fm": ar_fm, # Телефон и почта
},
).response
lead_id = bitrix_response.result["item"]["id"] # Идентификатор созданного лида
app.logger.info("Создан лид с ID %s", lead_id)
return jsonify({"message": "Лид создан", "id": lead_id})
except (BitrixAPIError, BitrixSDKException) as error:
# Подробности ошибки пишем в лог, посетителю их не показываем
app.logger.error(error)
return jsonify({"message": "Не удалось создать лид, попробуйте позже"}), 502
Метод возвращает данные созданного лида в объекте result.item.
Сокращенный ответ:
{
"result": {
"item": {
"id": 3465,
"title": "С сайта: Иван Иванов — ООО Ромашка",
"name": "Иван",
"lastName": "Иванов",
"companyTitle": "ООО Ромашка",
"entityTypeId": 1
}
}
}
Обработчик возвращает странице { "message": "Лид создан", "id": 3465 }. Идентификатор понадобится, чтобы открыть лид в интерфейсе или запросить его данные через REST.
Полный пример кода обработчика
// npm install express @bitrix24/b24jssdk
import express from 'express'
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
const app = express()
// Форма отправляет данные в формате application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }))
// Отдаем страницу с формой из папки public
app.use(express.static('public'))
// Шаблон для проверки адреса электронной почты
const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
// Ограничение длины значений: форма публичная
const maxLength = 100
// Обработчик принимает данные формы по маршруту /form
app.post('/form', async (req, res) => {
// Получаем данные из формы
const sName = String(req.body.NAME ?? '').trim()
const sLastName = String(req.body.LAST_NAME ?? '').trim()
const sCompanyTitle = String(req.body.COMPANY_TITLE ?? '').trim()
const sPhone = String(req.body.PHONE ?? '').trim()
const sEmail = String(req.body.EMAIL ?? '').trim()
// Проверяем данные до вызова метода
if (!sName) {
res.status(400).json({ message: 'Заполните имя' })
return
}
if (sEmail && !emailPattern.test(sEmail)) {
res.status(400).json({ message: 'Проверьте адрес электронной почты' })
return
}
if ([sName, sLastName, sCompanyTitle, sPhone, sEmail].some(value => value.length > maxLength)) {
res.status(400).json({ message: 'Одно из полей слишком длинное' })
return
}
// Собираем телефон и почту в мультиполя
const arFm = []
if (sPhone) {
arFm.push({ typeId: 'PHONE', valueType: 'WORK', value: sPhone })
}
if (sEmail) {
arFm.push({ typeId: 'EMAIL', valueType: 'HOME', value: sEmail })
}
// Формируем название лида из имени и фамилии
let sTitle = 'С сайта: ' + `${sName} ${sLastName}`.trim()
// Если есть название компании — добавляем его через тире после имени и фамилии
if (sCompanyTitle) {
sTitle += ' — ' + sCompanyTitle
}
// Отправляем данные в Битрикс24
try {
const response = await $b24.actions.v2.call.make({
method: 'crm.item.add',
params: {
entityTypeId: 1, // Тип объекта CRM — лид
fields: {
title: sTitle, // Название лида
name: sName, // Имя
lastName: sLastName, // Фамилия
companyTitle: sCompanyTitle, // Название компании
fm: arFm, // Телефон и почта
}
},
requestId: 'lead-add'
})
// Проверяем результат и выводим сообщение
if (!response.isSuccess) {
// Подробности ошибки пишем в лог, посетителю их не показываем
console.error(response.getErrorMessages().join('; '))
res.status(502).json({ message: 'Не удалось создать лид, попробуйте позже' })
return
}
const leadId = response.getData().result.item.id // Идентификатор созданного лида
console.info('Создан лид с ID ' + leadId)
res.json({ message: 'Лид создан', id: leadId })
} catch (error) {
// Сетевые ошибки и сбои SDK приходят исключением
console.error(error)
res.status(502).json({ message: 'Не удалось создать лид, попробуйте позже' })
}
})
// Запуск: node handler.mjs
app.listen(3000)
<?php
// composer require bitrix24/b24phpsdk:"^3.3"
// form.php и form.html лежат в публичном каталоге, vendor — выше него
// Локальный запуск: php -S localhost:3000 -t public
require_once __DIR__ . '/../vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
header('Content-Type: application/json; charset=utf-8');
// Ограничение длины значений: форма публичная
const MAX_LENGTH = 100;
// Обработчик принимает только POST-запросы
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['message' => 'Метод не поддерживается']);
exit;
}
$sb = ServiceBuilderFactory::createServiceBuilderFromWebhook(getenv('B24_HOOK'));
// Получаем данные из формы
$sName = trim((string)($_POST['NAME'] ?? ''));
$sLastName = trim((string)($_POST['LAST_NAME'] ?? ''));
$sCompanyTitle = trim((string)($_POST['COMPANY_TITLE'] ?? ''));
$sPhone = trim((string)($_POST['PHONE'] ?? ''));
$sEmail = trim((string)($_POST['EMAIL'] ?? ''));
// Проверяем данные до вызова метода
if ($sName === '') {
http_response_code(400);
echo json_encode(['message' => 'Заполните имя']);
exit;
}
if ($sEmail !== '' && !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/u', $sEmail)) {
http_response_code(400);
echo json_encode(['message' => 'Проверьте адрес электронной почты']);
exit;
}
foreach ([$sName, $sLastName, $sCompanyTitle, $sPhone, $sEmail] as $value) {
if (mb_strlen($value) > MAX_LENGTH) {
http_response_code(400);
echo json_encode(['message' => 'Одно из полей слишком длинное']);
exit;
}
}
// Собираем телефон и почту в мультиполя
$arFm = [];
if ($sPhone !== '') {
$arFm[] = ['typeId' => 'PHONE', 'valueType' => 'WORK', 'value' => $sPhone];
}
if ($sEmail !== '') {
$arFm[] = ['typeId' => 'EMAIL', 'valueType' => 'HOME', 'value' => $sEmail];
}
// Формируем название лида из имени и фамилии
$sTitle = 'С сайта: ' . trim($sName . ' ' . $sLastName);
// Если есть название компании — добавляем его через тире после имени и фамилии
if ($sCompanyTitle !== '') {
$sTitle .= ' — ' . $sCompanyTitle;
}
// Отправляем данные в Битрикс24
try {
$result = $sb->getCRMScope()->item()->add(1, [ // 1 — тип объекта CRM «Лид»
'title' => $sTitle, // Название лида
'name' => $sName, // Имя
'lastName' => $sLastName, // Фамилия
'companyTitle' => $sCompanyTitle, // Название компании
'fm' => $arFm, // Телефон и почта
]);
$leadId = $result->item()->id; // Идентификатор созданного лида
error_log('Создан лид с ID ' . $leadId);
echo json_encode(['message' => 'Лид создан', 'id' => $leadId]);
} catch (\Throwable $e) {
// Подробности ошибки пишем в лог, посетителю их не показываем
error_log($e->getMessage());
http_response_code(502);
echo json_encode(['message' => 'Не удалось создать лид, попробуйте позже']);
}
# pip install flask b24pysdk
import os
import re
from flask import Flask, request, jsonify
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError, BitrixSDKException
# Страницу form.html кладем в папку static
app = Flask(__name__)
client = Client(BitrixWebhook(
domain=os.environ["B24_DOMAIN"], # your-domain.bitrix24.ru
webhook_token=os.environ["B24_WEBHOOK_TOKEN"], # только user_id/token, без https://
))
# Шаблон для проверки адреса электронной почты
EMAIL_PATTERN = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+")
# Ограничение длины значений: форма публичная
MAX_LENGTH = 100
@app.route("/form", methods=["POST"])
def handle_form():
# Получаем данные из формы
s_name = request.form.get("NAME", "").strip()
s_last_name = request.form.get("LAST_NAME", "").strip()
s_company_title = request.form.get("COMPANY_TITLE", "").strip()
s_phone = request.form.get("PHONE", "").strip()
s_email = request.form.get("EMAIL", "").strip()
# Проверяем данные до вызова метода
if not s_name:
return jsonify({"message": "Заполните имя"}), 400
if s_email and not EMAIL_PATTERN.fullmatch(s_email):
return jsonify({"message": "Проверьте адрес электронной почты"}), 400
if any(len(value) > MAX_LENGTH for value in (s_name, s_last_name, s_company_title, s_phone, s_email)):
return jsonify({"message": "Одно из полей слишком длинное"}), 400
# Собираем телефон и почту в мультиполя
ar_fm = []
if s_phone:
ar_fm.append({"typeId": "PHONE", "valueType": "WORK", "value": s_phone})
if s_email:
ar_fm.append({"typeId": "EMAIL", "valueType": "HOME", "value": s_email})
# Формируем название лида из имени и фамилии
s_title = "С сайта: " + f"{s_name} {s_last_name}".strip()
# Если есть название компании — добавляем его через тире после имени и фамилии
if s_company_title:
s_title += " — " + s_company_title
# Отправляем данные в Битрикс24
try:
bitrix_response = client.crm.item.add(
entity_type_id=1, # Тип объекта CRM — лид
fields={
"title": s_title, # Название лида
"name": s_name, # Имя
"lastName": s_last_name, # Фамилия
"companyTitle": s_company_title, # Название компании
"fm": ar_fm, # Телефон и почта
},
).response
lead_id = bitrix_response.result["item"]["id"] # Идентификатор созданного лида
app.logger.info("Создан лид с ID %s", lead_id)
return jsonify({"message": "Лид создан", "id": lead_id})
except (BitrixAPIError, BitrixSDKException) as error:
# Подробности ошибки пишем в лог, посетителю их не показываем
app.logger.error(error)
return jsonify({"message": "Не удалось создать лид, попробуйте позже"}), 502
# Запуск: python handler.py
if __name__ == "__main__":
app.run(port=3000)
Проверим результат
-
Отправьте форму. В браузере появится сообщение вида «Лид создан. ID: 3465» — идентификатор понадобится на шаге 3
-
Откройте раздел CRM и перейдите в Лиды. Новый лид будет с названием «С сайта: Имя Фамилия», а если посетитель заполнил название компании — «С сайта: Имя Фамилия — Название компании»
-
Запросите данные лида методом crm.item.get. Передайте
entityTypeIdсо значением1иidиз ответа обработчика
Проверку выполните отдельным скриптом: он не зависит от обработчика и подключается к Битрикс24 через тот же вебхук.
// Сохраните в файл check.mjs в каталоге проекта рядом с node_modules
// Запуск: B24_HOOK='https://your-domain.bitrix24.ru/rest/1/TOKEN/' node check.mjs
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
const leadId = 3465 // Идентификатор из ответа обработчика
const response = await $b24.actions.v2.call.make({
method: 'crm.item.get',
params: { entityTypeId: 1, id: leadId },
requestId: 'lead-get'
})
console.info(response.getData().result.item)
<?php
// Сохраните в файл check.php в корне проекта рядом с каталогом vendor
// Запуск: B24_HOOK='https://your-domain.bitrix24.ru/rest/1/TOKEN/' php check.php
require_once __DIR__ . '/vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
$sb = ServiceBuilderFactory::createServiceBuilderFromWebhook(getenv('B24_HOOK'));
$leadId = 3465; // Идентификатор из ответа обработчика
$result = $sb->getCRMScope()->item()->get(1, $leadId);
print_r($result->item());
# Сохраните в файл check.py в каталоге проекта
# Запуск: B24_DOMAIN='your-domain.bitrix24.ru' B24_WEBHOOK_TOKEN='1/TOKEN' python check.py
import os
from b24pysdk import BitrixWebhook, Client
client = Client(BitrixWebhook(
domain=os.environ["B24_DOMAIN"],
webhook_token=os.environ["B24_WEBHOOK_TOKEN"],
))
lead_id = 3465 # Идентификатор из ответа обработчика
bitrix_response = client.crm.item.get(entity_type_id=1, bitrix_id=lead_id).response
print(bitrix_response.result["item"])
Сокращенный ответ:
{
"result": {
"item": {
"id": 3465,
"title": "С сайта: Иван Иванов — ООО Ромашка",
"name": "Иван",
"lastName": "Иванов",
"companyTitle": "ООО Ромашка",
"hasPhone": "Y",
"hasEmail": "Y",
"fm": [
{
"id": 11658,
"valueType": "WORK",
"value": "+70000000000",
"typeId": "PHONE"
},
{
"id": 11659,
"valueType": "HOME",
"value": "ivan@example.com",
"typeId": "EMAIL"
}
]
}
}
}
Значения name, lastName и companyTitle совпадают с полями формы. Телефон и почта возвращаются в массиве fm с теми типами typeId и valueType, которые передал обработчик. Флаги hasPhone и hasEmail показывают, что у лида есть заполненные телефон и почта — по ним видно, что мультиполя сохранились.
Ошибки и диагностика
Если метод вернул ошибку, проверьте данные запроса.
|
Код |
Причина и действие |
|
|
У пользователя, от имени которого создан вебхук, нет нужного права: на добавление лидов — для шага 2, на чтение лидов — для шага проверки. Проверьте права в настройках CRM |
|
|
На шаге 2 — передан неверный |
|
|
Неверное значение поля. Проверьте, что имена полей записаны в camelCase, а мультиполя переданы в |
|
|
В множественное поле передано не итерируемое значение. Убедитесь, что |
|
|
Неверный код вебхука. Проверьте значение переменной окружения с URL вебхука |
|
|
У вебхука нет scope |
|
|
Превышен лимит на интенсивность запросов. Повторите вызов позже |
Заполненность обязательных полей метод проверяет сам и возвращает ошибку, если поле пустое. Текст такой ошибки ищите в логе обработчика: посетителю уходит только общее сообщение. Молча метод игнорирует другое — неизвестное имя поля: опечатка в fields не вызывает ошибку, значение не сохранится. Поэтому после первого запуска сверьте сохраненные данные методом crm.item.get. Полный список ошибок метода приведен на странице crm.item.add.
Обработчик выполняет один вызов, поэтому после исправления повторите отправку формы целиком — незавершенных записей в CRM не остается. Сообщения «Заполните имя», «Проверьте адрес электронной почты» и «Одно из полей слишком длинное» возвращает сам обработчик со статусом 400, до обращения к Битрикс24.
Ошибки на участке между формой и обработчиком метод не возвращает — они видны в браузере.
|
Признак |
Причина и действие |
|
Обработчик завершается сразу при запуске или отвечает ошибкой на первый же запрос |
Не заданы переменные окружения с данными вебхука: |
|
Страница открыта из файловой системы по адресу вида |
Относительный путь из |
|
В консоли браузера ошибка CORS |
В |
|
В консоли браузера ошибка 404 |
Неверный адрес в переменной |
|
Ответ |
Файл |
|
Сообщение «Сервер вернул неожиданный ответ» |
Обработчик ответил не JSON: упал до |
Что важно учитывать
-
Каждая отправка формы создает новый лид. Если клиент обратится повторно, появятся дубли. Как их находить и связывать с имеющимися записями, описано в туториале Добавить повторный лид
-
Вебхук дает доступ ко всей CRM. Вызывайте REST только с сервера и не передавайте URL вебхука в браузер
-
Форму заполняют анонимные посетители. Длину значений обработчик из примера ограничивает, а защиту от автоматических отправок нужно добавить отдельно, например капчу
-
При переходе на другой тип объекта CRM меняется не только
entityTypeId: у каждого типа свой набор полей. Сверяйтесь с описанием параметраfieldsна странице crm.item.add -
Встроенные серверы из примеров —
app.listen,app.run,php -S— подходят для локальной проверки сценария. Публичную страницу размещайте на веб-сервере по HTTPS: форма собирает персональные данные клиента -
Сценарий использует универсальный метод crm.item.add. Развитие метода crm.lead.add остановлено: он продолжает работать, но в новых интеграциях используйте
crm.item.add