Add Repeat Lead
Scope:
crmWho can execute the method: users with read access permission for leads, contacts, companies, and permission to create leads.
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.
When a customer fills out a form on a site, their data is sent to a handler. The script searches the CRM for matches by phone number or email address among leads, contacts, and companies. If matches are found, the lead is marked as a repeat lead and linked to the existing record. This approach helps avoid duplicates and increases manager efficiency.
Repeat lead mode must be enabled in Bitrix24. For more details, read the article Repeat Leads and Deals.
The setup consists of two stages:
-
Prepare the fields and place the form on the page.
-
Create a handler file that sequentially calls the crm.duplicate.findbycomm, crm.lead.list, and crm.lead.add methods.
1. Create a Web Form
Create an HTML form with the following fields:
-
NAME— customer name, required field, -
LAST_NAME— surname, -
PHONE— phone, -
EMAIL— Email.
The form sends data to the handler using the POST method.
<form id="form_to_crm">
<input type="text" name="NAME" placeholder="Name" required>
<input type="text" name="LAST_NAME" placeholder="Last name">
<input type="text" name="PHONE" placeholder="Phone">
<input type="text" name="EMAIL" placeholder="E-mail">
<input type="submit" value="Submit">
</form>
<script>
document.getElementById('form_to_crm').addEventListener('submit', async (el) => {
el.preventDefault();
const formData = Object.fromEntries(new FormData(el.currentTarget).entries());
const response = await fetch('/form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await response.json();
alert(data.message);
});
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#form_to_crm').on( 'submit', function(el) {//event submit form
el.preventDefault();//the default action of the event will not be triggered
var formData = $(this).serialize();
$.ajax({
'method': 'POST',
'dataType': 'json',
'url': 'form.php', // file for saving filled forms
'data': formData,
success: function(data){//success callback
alert(data.message);
}
});
});
});
</script>
<form id="form_to_crm">
<input type="text" name="NAME" placeholder="Name" required>
<input type="text" name="LAST_NAME" placeholder="Last name">
<input type="text" name="PHONE" placeholder="Phone">
<input type="text" name="EMAIL" placeholder="E-mail">
<input type="submit" value="Submit">
</form>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#form_to_crm').on( 'submit', function(el) {//event submit form
el.preventDefault();//the default action of the event will not be triggered
var formData = $(this).serialize();
$.ajax({
'method': 'POST',
'dataType': 'json',
'url': '/form', // Flask handler route
'data': formData,
success: function(data){//success callback
alert(data.message);
}
});
});
});
</script>
<form id="form_to_crm">
<input type="text" name="NAME" placeholder="Name" required>
<input type="text" name="LAST_NAME" placeholder="Last name">
<input type="text" name="PHONE" placeholder="Phone">
<input type="text" name="EMAIL" placeholder="E-mail">
<input type="submit" value="Submit">
</form>
2. Creating a Form Handler
Create a handler. It will process the data, check for duplicates, and create a lead.
Retrieving Data from the Form
Retrieve and safely process data from the NAME, LAST_NAME, PHONE, and EMAIL fields to prevent XSS attacks.
const sName = String(req.body.NAME ?? '')
const sLastName = String(req.body.LAST_NAME ?? '')
const sPhone = String(req.body.PHONE ?? '')
const sEmail = String(req.body.EMAIL ?? '')
$sName = htmlspecialchars($_POST["NAME"]);
$sLastName = htmlspecialchars($_POST["LAST_NAME"]);
$sPhone = htmlspecialchars($_POST["PHONE"]);
$sEmail = htmlspecialchars($_POST["EMAIL"]);
s_name = request.form.get("NAME", "")
s_last_name = request.form.get("LAST_NAME", "")
s_phone = request.form.get("PHONE", "")
s_email = request.form.get("EMAIL", "")
Form an $arFields array with the new lead data.
const arFields = {
TITLE: 'From the site: ' + [sName, sLastName].join(' '),
NAME: sName || 'Empty name',
LAST_NAME: sLastName,
PHONE: sPhone ? [{ VALUE: sPhone, VALUE_TYPE: 'HOME' }] : [],
EMAIL: sEmail ? [{ VALUE: sEmail, VALUE_TYPE: 'HOME' }] : [],
}
$arFields = [
'TITLE' => 'From the site: ' . implode(' ', [$sName, $sLastName]),
'NAME' => (!empty($sName)) ? $sName : 'Empty name',
'LAST_NAME' => $sLastName,
'PHONE' => (!empty($sPhone)) ? array(array('VALUE' => $sPhone, 'VALUE_TYPE' => 'HOME')) : array(),
'EMAIL' => (!empty($sEmail)) ? array(array('VALUE' => $sEmail, 'VALUE_TYPE' => 'HOME')) : array()
];
ar_fields = {
"TITLE": "From the site: " + " ".join([s_name, s_last_name]),
"NAME": s_name or "Empty name",
"LAST_NAME": s_last_name,
"PHONE": [{"VALUE": s_phone, "VALUE_TYPE": "HOME"}] if s_phone else [],
"EMAIL": [{"VALUE": s_email, "VALUE_TYPE": "HOME"}] if s_email else [],
}
Form the lead heading as From the site: First Name Last Name.
The system stores phone numbers and email addresses as arrays of crm_multifield objects, so we form the PHONE and EMAIL arrays using the $sPhone and $sEmail values.
-
Write
$sPhoneand$sEmailto theVALUEfields. -
Pass types, such as
HOME, to theVALUE_TYPEfields.
If the $sPhone and $sEmail variables do not contain values, specify empty arrays.
Searching for Lead Duplicates
To find repeat leads by phone number and email address, call the crm.duplicate.findbycomm method twice. You must pass the following data to it:
-
entity_type— the object type. PassLEAD— lead. -
type— the communication type. SpecifyPHONEduring the first call andEMAILduring the second. -
values— an array of values to search for. Pass the phone number value$sPhoneretrieved from the form.
Searching by phone, "type" => "PHONE".
if (sPhone) {
const resultDuplicate = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { entity_type: 'LEAD', type: 'PHONE', values: [sPhone] },
requestId: 'dup-phone'
})
const found = resultDuplicate.getData()?.result?.LEAD
if (found) arLeadDuplicate = arLeadDuplicate.concat(found)
}
if (!empty($sPhone)) {
$result = $sb->getCRMScope()->duplicate()->findByPhone(
[$sPhone],
\Bitrix24\SDK\Services\CRM\Duplicates\Service\EntityType::Lead
)->getCoreResponse()->getResponseData()->getResult();
if (!empty($result['LEAD'])) {
$arLeadDuplicate = array_merge($arLeadDuplicate, $result['LEAD']);
}
}
if s_phone:
result_duplicate = client.crm.duplicate.findbycomm(
type="PHONE", values=[s_phone], entity_type="LEAD",
).result
if result_duplicate.get("LEAD"):
ar_lead_duplicate += result_duplicate["LEAD"]
Searching for duplicates by email, "type" => "EMAIL".
if (sEmail) {
const resultDuplicate = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { entity_type: 'LEAD', type: 'EMAIL', values: [sEmail] },
requestId: 'dup-email'
})
const found = resultDuplicate.getData()?.result?.LEAD
if (found) arLeadDuplicate = arLeadDuplicate.concat(found)
}
if (!empty($sEmail)) {
$result = $sb->getCRMScope()->duplicate()->findByEmail(
[$sEmail],
\Bitrix24\SDK\Services\CRM\Duplicates\Service\EntityType::Lead
)->getCoreResponse()->getResponseData()->getResult();
if (!empty($result['LEAD'])) {
$arLeadDuplicate = array_merge($arLeadDuplicate, $result['LEAD']);
}
}
if s_email:
result_duplicate = client.crm.duplicate.findbycomm(
type="EMAIL", values=[s_email], entity_type="LEAD",
).result
if result_duplicate.get("LEAD"):
ar_lead_duplicate += result_duplicate["LEAD"]
Combine the identifiers of the found duplicates into the $arLeadDuplicate array.
Processing Duplicates
If duplicates are found, call the crm.lead.list method.
-
Apply a filter by identifier and status
CONVERTED. -
Select the fields:
ID,COMPANY_ID,, andCONTACT_ID. -
Retain the result in the
$arDuplicateLeadarray. -
Fill the
COMPANY_IDandCONTACT_IDfields in the$arFieldsarray with values from$arDuplicateLead.
if (arLeadDuplicate.length) {
const duplicateLead = await $b24.actions.v2.callList.make({
method: 'crm.lead.list',
params: {
filter: { '=ID': arLeadDuplicate, STATUS_ID: 'CONVERTED' },
select: ['ID', 'COMPANY_ID', 'CONTACT_ID']
},
requestId: 'dup-lead-list'
})
const arDuplicateLead = duplicateLead.getData()?.result ?? []
const company = arDuplicateLead.map(r => r.COMPANY_ID).find(v => v > 0)
const contact = arDuplicateLead.map(r => r.CONTACT_ID).find(v => v > 0)
if (company) arFields.COMPANY_ID = company
if (contact) arFields.CONTACT_ID = contact
}
if (!empty($arLeadDuplicate)) {
$arDuplicateLead = [];
foreach ($sb->getCRMScope()->lead()->batch->list(
order: [],
filter: ['=ID' => $arLeadDuplicate, 'STATUS_ID' => 'CONVERTED'],
select: ['ID', 'COMPANY_ID', 'CONTACT_ID']
) as $lead) {
$arDuplicateLead[] = $lead;
}
foreach ($arDuplicateLead as $lead) {
if ($lead->COMPANY_ID > 0 && empty($arFields['COMPANY_ID'])) {
$arFields['COMPANY_ID'] = $lead->COMPANY_ID;
}
if ($lead->CONTACT_ID > 0 && empty($arFields['CONTACT_ID'])) {
$arFields['CONTACT_ID'] = $lead->CONTACT_ID;
}
}
}
if ar_lead_duplicate:
ar_duplicate_lead = client.crm.lead.list(
filter={"=ID": ar_lead_duplicate, "STATUS_ID": "CONVERTED"},
select=["ID", "COMPANY_ID", "CONTACT_ID"],
).as_list().result
company = next((r["COMPANY_ID"] for r in ar_duplicate_lead if int(r["COMPANY_ID"] or 0) > 0), None)
contact = next((r["CONTACT_ID"] for r in ar_duplicate_lead if int(r["CONTACT_ID"] or 0) > 0), None)
if company:
ar_fields["COMPANY_ID"] = company
if contact:
ar_fields["CONTACT_ID"] = contact
Adding a New Lead
To add a lead, use the crm.lead.add method. Pass the $arFields array to it.
Check which mandatory fields are configured for leads in your Bitrix24. All mandatory fields must be passed to the crm.lead.add method.
await $b24.actions.v2.call.make({
method: 'crm.lead.add',
params: { fields: arFields },
requestId: 'repeat-lead-add'
})
$sb->getCRMScope()->lead()->add($arFields);
client.crm.lead.add(fields=ar_fields)
If the lead is created successfully, the method will return its identifier. If you receive error error, review the possible error descriptions in the crm.lead.add method documentation.
{
"result": 3289,
}
Full Handler Code Example
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.json())
// Handler accepts form data via the /form route
app.post('/form', async (req, res) => {
const sName = String(req.body.NAME ?? '')
const sLastName = String(req.body.LAST_NAME ?? '')
const sPhone = String(req.body.PHONE ?? '')
const sEmail = String(req.body.EMAIL ?? '')
const arFields = {
TITLE: sName || 'Empty name',
LAST_NAME: sLastName,
PHONE: sPhone ? [{ VALUE: sPhone, VALUE_TYPE: 'HOME' }] : [],
EMAIL: sEmail ? [{ VALUE: sEmail, VALUE_TYPE: 'HOME' }] : [],
}
let arLeadDuplicate = []
if (sPhone) { // search for duplicates by phone number
const r = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { entity_type: 'LEAD', type: 'PHONE', values: [sPhone] },
requestId: 'dup-phone'
})
const found = r.getData()?.result?.LEAD
if (found) arLeadDuplicate = arLeadDuplicate.concat(found)
}
if (sEmail) { // search for duplicates by email
const r = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { entity_type: 'LEAD', type: 'EMAIL', values: [sEmail] },
requestId: 'dup-email'
})
const found = r.getData()?.result?.LEAD
if (found) arLeadDuplicate = arLeadDuplicate.concat(found)
}
if (arLeadDuplicate.length) { // get lead duplicate with fields of associated contact and company
const duplicateLead = await $b24.actions.v2.callList.make({
method: 'crm.lead.list',
params: {
filter: { '=ID': arLeadDuplicate, STATUS_ID: 'CONVERTED' },
select: ['ID', 'COMPANY_ID', 'CONTACT_ID']
},
requestId: 'dup-lead-list'
})
const arDuplicateLead = duplicateLead.getData()?.result ?? []
const company = arDuplicateLead.map(r => r.COMPANY_ID).find(v => v > 0)
const contact = arDuplicateLead.map(r => r.CONTACT_ID).find(v => v > 0)
if (company) arFields.COMPANY_ID = company
if (contact) arFields.CONTACT_ID = contact
}
const result = await $b24.actions.v2.call.make({ // create repeat lead
method: 'crm.lead.add',
params: { fields: arFields },
requestId: 'repeat-lead-add'
})
if (result.isSuccess && result.getData()?.result) {
res.json({ message: 'Lead add' })
} else {
res.json({ message: 'Lead not added: ' + result.getErrorMessages().join('; ') })
}
})
app.listen(3000)
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Bitrix24\SDK\Services\CRM\Duplicates\Service\EntityType;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$sb = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/');
$sName = htmlspecialchars($_POST["NAME"]);
$sLastName = htmlspecialchars($_POST["LAST_NAME"]);
$sPhone = htmlspecialchars($_POST["PHONE"]);
$sEmail = htmlspecialchars($_POST["EMAIL"]);
$arFields = [
'TITLE' => (!empty($sName)) ? $sName : 'Empty name',
'LAST_NAME' => $sLastName,
'PHONE' => (!empty($sPhone)) ? array(array('VALUE' => $sPhone, 'VALUE_TYPE' => 'HOME')) : array(),
'EMAIL' => (!empty($sEmail)) ? array(array('VALUE' => $sEmail, 'VALUE_TYPE' => 'HOME')) : array()
];
$arLeadDuplicate = [];
if (!empty($sPhone)) { // search for duplicates by phone number
$r = $sb->getCRMScope()->duplicate()->findByPhone([$sPhone], EntityType::Lead)
->getCoreResponse()->getResponseData()->getResult();
if (!empty($r['LEAD'])) {
$arLeadDuplicate = array_merge($arLeadDuplicate, $r['LEAD']);
}
}
if (!empty($sEmail)) { // search for duplicates by email
$r = $sb->getCRMScope()->duplicate()->findByEmail([$sEmail], EntityType::Lead)
->getCoreResponse()->getResponseData()->getResult();
if (!empty($r['LEAD'])) {
$arLeadDuplicate = array_merge($arLeadDuplicate, $r['LEAD']);
}
}
if (!empty($arLeadDuplicate)) { // get lead duplicate with fields of associated contact and company
foreach ($sb->getCRMScope()->lead()->batch->list(
order: [],
filter: ['=ID' => $arLeadDuplicate, 'STATUS_ID' => 'CONVERTED'],
select: ['ID', 'COMPANY_ID', 'CONTACT_ID']
) as $lead) {
if ($lead->COMPANY_ID > 0 && empty($arFields['COMPANY_ID'])) {
$arFields['COMPANY_ID'] = $lead->COMPANY_ID;
}
if ($lead->CONTACT_ID > 0 && empty($arFields['CONTACT_ID'])) {
$arFields['CONTACT_ID'] = $lead->CONTACT_ID;
}
}
}
try {
$sb->getCRMScope()->lead()->add($arFields); // create repeat lead
echo json_encode(['message' => 'Lead add']);
} catch (\Throwable $e) {
echo json_encode(['message' => 'Lead not added: ' . $e->getMessage()]);
}
# pip install b24pysdk
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 handle_form():
s_name = request.form.get("NAME", "")
s_last_name = request.form.get("LAST_NAME", "")
s_phone = request.form.get("PHONE", "")
s_email = request.form.get("EMAIL", "")
ar_fields = {
"TITLE": s_name or "Empty name",
"LAST_NAME": s_last_name,
"PHONE": [{"VALUE": s_phone, "VALUE_TYPE": "HOME"}] if s_phone else [],
"EMAIL": [{"VALUE": s_email, "VALUE_TYPE": "HOME"}] if s_email else [],
}
ar_lead_duplicate = []
if s_phone: # search for duplicates by phone number
r = client.crm.duplicate.findbycomm(type="PHONE", values=[s_phone], entity_type="LEAD").result
if r.get("LEAD"):
ar_lead_duplicate += r["LEAD"]
if s_email: # search for duplicates by email
r = client.crm.duplicate.findbycomm(type="EMAIL", values=[s_email], entity_type="LEAD").result
if r.get("LEAD"):
ar_lead_duplicate += r["LEAD"]
if ar_lead_duplicate: # get lead duplicate with fields of associated contact and company
ar_duplicate_lead = client.crm.lead.list(
filter={"=ID": ar_lead_duplicate, "STATUS_ID": "CONVERTED"},
select=["ID", "COMPANY_ID", "CONTACT_ID"],
).as_list().result
company = next((r["COMPANY_ID"] for r in ar_duplicate_lead if int(r["COMPANY_ID"] or 0) > 0), None)
contact = next((r["CONTACT_ID"] for r in ar_duplicate_lead if int(r["CONTACT_ID"] or 0) > 0), None)
if company:
ar_fields["COMPANY_ID"] = company
if contact:
ar_fields["CONTACT_ID"] = contact
try:
client.crm.lead.add(fields=ar_fields) # create repeat lead
return jsonify({"message": "Lead add"})
except Exception as e:
return jsonify({"message": f"Lead not added: {e}"})