How to Find Duplicates in CRM by Phone and Email
Scope:
crmWho can execute methods: users with read access to CRM entities
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect the MCP server so the assistant can utilize the official REST documentation.
You can automate the search for duplicates by phone and email address using a script. It will find leads, contacts, and companies with matching data, retrieve information about them, and display it in a table:
-
object identifier,
-
object type: lead, contact, or company,
-
heading or first and last name,
-
phone,
-
email address.
To find duplicates, we will call the following methods sequentially:
-
crm.duplicate.findbycomm — find duplicates by phone and email,
-
crm.lead.list — retrieve leads,
-
crm.contact.list — retrieve contacts,
-
crm.company.list — retrieve companies.
Prepare the Data
We will pass the phone number and email to the script using dialog boxes in the browser. The values will be stored in the variables phone and email.
If the data is entered correctly and duplicates are found, they will be displayed in the table. Otherwise, the table will be empty.
We will create arrays:
-
entityIDs— identifiers of the found leads, contacts, and companies, -
$resultEntity— detailed data about the found objects.
How to Use Examples in Documentation
let phone = prompt("Enter phone number:");
let email = prompt("Enter email:");
let entityIDs = {
'LEAD': [],
'CONTACT': [],
'COMPANY': []
};
let resultEntity = {
'lead': [],
'contact': [],
'company': []
};
$phone = readline("Enter phone number: ");
$email = readline("Enter email: ");
$entityIDs = [
'LEAD' => [],
'CONTACT' => [],
'COMPANY' => []
];
$resultEntity = [
'lead' => [],
'contact' => [],
'company' => []
];
phone = input("Enter phone number: ")
email = input("Enter email: ")
entity_ids = {
"LEAD": [],
"CONTACT": [],
"COMPANY": [],
}
result_entity = {
"lead": [],
"contact": [],
"company": [],
}
// The phone and the email to search by. Neighbouring tabs ask them from
// the user; here they are set by constants, because the example itself creates
// objects with these values.
const (
phone = "+49 900 555-35-35"
email = "duplicate@example.com"
)
// The IDs of the found objects and their detailed data. The keys are
// the same ones that crm.duplicate.findbycomm returns.
entityIDs := map[string][]b24.ID{"LEAD": nil, "CONTACT": nil, "COMPANY": nil}
rows := make([]row, 0)
1. Find Duplicate Objects
To find duplicate objects by phone and email, we will call the method crm.duplicate.findbycomm twice. We will pass two parameters to it.
-
type— communication type,PHONEorEMAIL. -
values— an array of phone numbers or email addresses. We will specify variablesphoneandemail.
We will combine the identifiers of the found duplicates into the entityIDs array.
// Merges identifiers from the method response with the entityIDs object
function mergeDuplicates(data) {
for (const type of ['LEAD', 'CONTACT', 'COMPANY']) {
if (Array.isArray(data?.[type])) {
entityIDs[type] = entityIDs[type].concat(data[type]);
}
}
}
if (phone) {
const phoneResult = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { type: 'PHONE', values: [phone] }
});
if (phoneResult.isSuccess) {
mergeDuplicates(phoneResult.getData()?.result);
} else {
console.error('Error searching for duplicates by phone:', phoneResult.getErrorMessages().join('; '));
}
}
if (email) {
const emailResult = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { type: 'EMAIL', values: [email] }
});
if (emailResult.isSuccess) {
mergeDuplicates(emailResult.getData()?.result);
} else {
console.error('Error searching for duplicates by email:', emailResult.getErrorMessages().join('; '));
}
}
use Bitrix24\SDK\Services\CRM\Duplicates\Result\DuplicateResult;
// Merges identifiers from the method response with the $entityIDs array
function mergeDuplicates(DuplicateResult $result, array &$entityIDs): void
{
$data = $result->getCoreResponse()->getResponseData()->getResult();
foreach (['LEAD', 'CONTACT', 'COMPANY'] as $type) {
if (!empty($data[$type]) && is_array($data[$type])) {
$entityIDs[$type] = array_merge($entityIDs[$type], $data[$type]);
}
}
}
if($phone)
{
mergeDuplicates($sb->getCRMScope()->duplicate()->findByPhone([$phone]), $entityIDs);
}
if($email)
{
mergeDuplicates($sb->getCRMScope()->duplicate()->findByEmail([$email]), $entityIDs);
}
if phone:
result = client.crm.duplicate.findbycomm(
type="PHONE",
values=[phone],
).result
for key in entity_ids:
if isinstance(result.get(key), list):
entity_ids[key].extend(result[key])
if email:
result = client.crm.duplicate.findbycomm(
type="EMAIL",
values=[email],
).result
for key in entity_ids:
if isinstance(result.get(key), list):
entity_ids[key].extend(result[key])
// The method searches by ONE communication type per call, so the phone and the email are
// queried separately, and the IDs are accumulated in a shared map.
for _, comm := range []struct{ typ, value string }{
{"PHONE", phone},
{"EMAIL", email},
} {
if comm.value == "" {
continue
}
res, err := core.Call(ctx, "crm.duplicate.findbycomm", b24.Params{
"type": comm.typ,
"values": []string{comm.value},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.duplicate.findbycomm %s: %w", comm.typ, err)
}
// The response is an object with the LEAD, CONTACT, and COMPANY keys. A key may be absent
// altogether: if nothing was found for that type, it is simply not sent.
var found map[string][]b24.ID
if err := json.Unmarshal(res.Result, &found); err != nil {
return fmt.Errorf("parse duplicates: %w", err)
}
for key := range entityIDs {
entityIDs[key] = appendUnique(entityIDs[key], found[key])
}
}
The crm.duplicate.findbycomm method returns lists of identifiers for leads, contacts, and companies where the specified phone number or email address is found.
2. Retrieve Leads
If the list of lead identifiers is not empty, we will retrieve their data using the crm.lead.list method.
-
Apply a filter by identifier.
-
Select the fields:
ID,NAME,LAST_NAME,PHONE,EMAIL,TITLE. -
Save the result in the
resultEntityarray.
if (entityIDs.LEAD.length > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.lead.list',
params: {
filter: { ID: entityIDs.LEAD },
select: ['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL', 'TITLE']
}
});
if (result.isSuccess) {
const leads = result.getData()?.result;
if (leads?.length > 0) {
resultEntity.lead = leads;
}
} else {
console.error(result.getErrorMessages().join('; '));
}
}
if(!empty($entityIDs['LEAD']))
{
$leads = $sb->getCRMScope()->lead()->list(
[],
['ID' => $entityIDs['LEAD']],
['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL', 'TITLE']
)->getLeads();
if(!empty($leads))
{
$resultEntity['lead'] = $leads;
}
}
if entity_ids["LEAD"]:
result = client.crm.lead.list(
filter={"ID": entity_ids["LEAD"]},
select=["ID", "NAME", "LAST_NAME", "PHONE", "EMAIL", "TITLE"],
).result
if result:
result_entity["lead"] = result
res, err := core.Call(ctx, "crm.lead.list", b24.Params{
"filter": b24.Params{"ID": entityIDs["LEAD"]},
"select": []string{"ID", "NAME", "LAST_NAME", "PHONE", "EMAIL", "TITLE"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.lead.list: %w", err)
}
var leads []entity
if err := json.Unmarshal(res.Result, &leads); err != nil {
return fmt.Errorf("parse leads: %w", err)
}
for _, e := range leads {
rows = append(rows, e.row("lead"))
}
The method crm.lead.list will return a list of leads based on the filter.
{
"result":[{
"ID":"1183",
"NAME":null,
"LAST_NAME":null,
"TITLE":"Filling out the CRM form \"Contact data form for open lines\"",
"PHONE":[{
"ID":"1957",
"VALUE_TYPE":"OTHER",
"VALUE":"+493216464646",
"TYPE_ID":"PHONE"
}]
}]
}
3. Retrieve Contacts
If the list of contact IDs is not empty, we will retrieve their data using the method crm.contact.list.
-
Apply a filter by identifier.
-
Select the fields:
ID,NAME,LAST_NAME,PHONE,EMAIL. -
Save the result in the
resultEntityarray.
if (entityIDs.CONTACT.length > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.contact.list',
params: {
filter: { ID: entityIDs.CONTACT },
select: ['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL']
}
});
if (result.isSuccess) {
const contacts = result.getData()?.result;
if (contacts?.length > 0) {
resultEntity.contact = contacts;
}
} else {
console.error(result.getErrorMessages().join('; '));
}
}
if(!empty($entityIDs['CONTACT']))
{
$contacts = $sb->getCRMScope()->contact()->list(
[],
['ID' => $entityIDs['CONTACT']],
['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL'],
0
)->getContacts();
if(!empty($contacts))
{
$resultEntity['contact'] = $contacts;
}
}
if entity_ids["CONTACT"]:
result = client.crm.contact.list(
filter={"ID": entity_ids["CONTACT"]},
select=["ID", "NAME", "LAST_NAME", "PHONE", "EMAIL"],
).result
if result:
result_entity["contact"] = result
res, err := core.Call(ctx, "crm.contact.list", b24.Params{
"filter": b24.Params{"ID": entityIDs["CONTACT"]},
"select": []string{"ID", "NAME", "LAST_NAME", "PHONE", "EMAIL"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.contact.list: %w", err)
}
var contacts []entity
if err := json.Unmarshal(res.Result, &contacts); err != nil {
return fmt.Errorf("parse contacts: %w", err)
}
for _, e := range contacts {
rows = append(rows, e.row("contact"))
}
The crm.contact.list method returns a list of contacts by filter.
{
"result":[{
"ID":"23",
"NAME":"Klaus",
"LAST_NAME":"Weber",
"EMAIL":[{
"ID":"854",
"VALUE_TYPE":"WORK",
"VALUE":"alekseev@ya.com",
"TYPE_ID":"EMAIL"
}]
}]
}
4. Retrieve Companies
If the list of company IDs is not empty, we will retrieve their data using the method crm.company.list.
-
Apply a filter by identifier.
-
Select the following fields:
ID,PHONE,EMAIL,TITLE. -
Retain the result in the
resultEntityarray.
if (entityIDs.COMPANY.length > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.company.list',
params: {
filter: { ID: entityIDs.COMPANY },
select: ['ID', 'PHONE', 'EMAIL', 'TITLE']
}
});
if (result.isSuccess) {
const companies = result.getData()?.result;
if (companies?.length > 0) {
resultEntity.company = companies;
}
} else {
console.error(result.getErrorMessages().join('; '));
}
}
if(!empty($entityIDs['COMPANY']))
{
$companies = $sb->getCRMScope()->company()->list(
[],
['ID' => $entityIDs['COMPANY']],
['ID', 'PHONE', 'EMAIL', 'TITLE']
)->getCompanies();
if(!empty($companies))
{
$resultEntity['company'] = $companies;
}
}
if entity_ids["COMPANY"]:
result = client.crm.company.list(
filter={"ID": entity_ids["COMPANY"]},
select=["ID", "PHONE", "EMAIL", "TITLE"],
).result
if result:
result_entity["company"] = result
// A company has no first or last name — its name is in TITLE.
res, err := core.Call(ctx, "crm.company.list", b24.Params{
"filter": b24.Params{"ID": entityIDs["COMPANY"]},
"select": []string{"ID", "TITLE", "PHONE", "EMAIL"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.company.list: %w", err)
}
var companies []entity
if err := json.Unmarshal(res.Result, &companies); err != nil {
return fmt.Errorf("parse companies: %w", err)
}
for _, e := range companies {
rows = append(rows, e.row("company"))
}
The method crm.company.list will return a list of companies based on the filter.
{
"result":[{
"ID":"587",
"TITLE":"Romanka",
"PHONE":[{
"ID":"1899",
"VALUE_TYPE":"WORK",
"VALUE":"5345654",
"TYPE_ID":"PHONE"
}],
"EMAIL":[{
"ID":"1901",
"VALUE_TYPE":"WORK",
"VALUE":"company@xample.com",
"TYPE_ID":"EMAIL"}]
}]
}
Display Results in a Table
Display the found records in the Identifier, Object type, Name/First and last name, Phone, Email columns.
let table = [];
table.push([
"Identifier",
"Object type",
"Name/First and last name",
"Phone",
"Email"
]);
for (let entity in resultEntity) {
resultEntity[entity].forEach(function(item) {
let phones = '';
if (item.PHONE) {
phones = item.PHONE.map(phone => phone.VALUE).join(', ');
}
let emails = '';
if (item.EMAIL) {
emails = item.EMAIL.map(email => email.VALUE).join(', ');
}
let title = item.TITLE ? item.TITLE + (item.NAME || item.LAST_NAME ? ': ' : '') : '';
if (item.NAME || item.LAST_NAME) {
title += [item.NAME, item.LAST_NAME].join(' ');
}
table.push([
item.ID,
entity,
title,
phones || '—',
emails || '—'
]);
});
}
console.table(table);
$table = [];
$table[] = [
"Identifier",
"Object type",
"Name/First and last name",
"Phone",
"Email"
];
foreach ($resultEntity as $entityType => $entities) {
foreach ($entities as $item) {
$phones = '';
if (!empty($item->PHONE)) {
$phones = implode(', ', array_map(fn($phone) => $phone->VALUE, $item->PHONE));
}
$emails = '';
if (!empty($item->EMAIL)) {
$emails = implode(', ', array_map(fn($email) => $email->VALUE, $item->EMAIL));
}
$title = !empty($item->TITLE) ? $item->TITLE : '';
if (!empty($item->NAME) || !empty($item->LAST_NAME)) {
$namePart = trim($item->NAME . ' ' . $item->LAST_NAME);
if ($title) {
$title .= ': ' . $namePart;
} else {
$title = $namePart;
}
}
$table[] = [
$item->ID,
$entityType,
$title ?: '—',
$phones ?: '—',
$emails ?: '—'
];
}
}
foreach ($table as $row) {
echo implode("\t", $row) . "\n";
}
table = [[
"Identifier",
"Object type",
"Name/First and last name",
"Phone",
"Email",
]]
for entity_type, entities in result_entity.items():
for item in entities:
phones = ", ".join(phone["VALUE"] for phone in (item.get("PHONE") or []))
emails = ", ".join(email["VALUE"] for email in (item.get("EMAIL") or []))
title = item.get("TITLE") or ""
name_part = " ".join(filter(None, [item.get("NAME"), item.get("LAST_NAME")]))
if title and name_part:
title = f"{title}: {name_part}"
elif name_part:
title = name_part
table.append([
item["ID"],
entity_type,
title or "—",
phones or "—",
emails or "—",
])
for row in table:
print("\t".join(map(str, row)))
if len(rows) == 0 {
fmt.Println("No duplicates found")
return nil
}
fmt.Println("ID\tType\tName\tPhone\tEmail")
for _, r := range rows {
fmt.Printf("%d\t%s\t%s\t%s\t%s\n", r.ID, r.Kind, r.Title, r.Phone, r.Email)
}
Code Example
import { createInterface } from 'node:readline/promises'
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// Requesting phone and email from the user
const rl = createInterface({ input: process.stdin, output: process.stdout })
const phone = await rl.question('Enter phone number: ')
const email = await rl.question('Enter email: ')
rl.close()
// Initializing variables
const entityIDs = {
LEAD: [],
CONTACT: [],
COMPANY: []
}
const resultEntity = {
lead: [],
contact: [],
company: []
}
// Merges identifiers from the method response with the entityIDs object
function mergeDuplicates(data) {
for (const type of ['LEAD', 'CONTACT', 'COMPANY']) {
if (Array.isArray(data?.[type])) {
entityIDs[type] = entityIDs[type].concat(data[type])
}
}
}
// Searching for duplicates by phone
if (phone) {
const phoneResult = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { type: 'PHONE', values: [phone] }
})
if (phoneResult.isSuccess) {
mergeDuplicates(phoneResult.getData()?.result)
} else {
console.error('Error searching for duplicates by phone:', phoneResult.getErrorMessages().join('; '))
}
}
// Searching for duplicates by email
if (email) {
const emailResult = await $b24.actions.v2.call.make({
method: 'crm.duplicate.findbycomm',
params: { type: 'EMAIL', values: [email] }
})
if (emailResult.isSuccess) {
mergeDuplicates(emailResult.getData()?.result)
} else {
console.error('Error searching for duplicates by email:', emailResult.getErrorMessages().join('; '))
}
}
// Processing leads
if (entityIDs.LEAD.length > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.lead.list',
params: {
filter: { ID: entityIDs.LEAD },
select: ['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL', 'TITLE']
}
})
if (result.isSuccess) {
const leads = result.getData()?.result
if (leads?.length > 0) {
resultEntity.lead = leads
}
} else {
console.error(result.getErrorMessages().join('; '))
}
}
// Processing contacts
if (entityIDs.CONTACT.length > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.contact.list',
params: {
filter: { ID: entityIDs.CONTACT },
select: ['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL']
}
})
if (result.isSuccess) {
const contacts = result.getData()?.result
if (contacts?.length > 0) {
resultEntity.contact = contacts
}
} else {
console.error(result.getErrorMessages().join('; '))
}
}
// Processing companies
if (entityIDs.COMPANY.length > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.company.list',
params: {
filter: { ID: entityIDs.COMPANY },
select: ['ID', 'PHONE', 'EMAIL', 'TITLE']
}
})
if (result.isSuccess) {
const companies = result.getData()?.result
if (companies?.length > 0) {
resultEntity.company = companies
}
} else {
console.error(result.getErrorMessages().join('; '))
}
}
// Forming the table
const table = []
// Table header
table.push([
"Identifier",
"Object type",
"Name/First and last name",
"Phone",
"Email"
])
// Data rows
for (const entity in resultEntity) {
resultEntity[entity].forEach(function(item) {
let phones = ''
if (item.PHONE) {
phones = item.PHONE.map(phone => phone.VALUE).join(', ')
}
let emails = ''
if (item.EMAIL) {
emails = item.EMAIL.map(email => email.VALUE).join(', ')
}
let title = item.TITLE ? item.TITLE + (item.NAME || item.LAST_NAME ? ': ' : '') : ''
if (item.NAME || item.LAST_NAME) {
title += [item.NAME, item.LAST_NAME].join(' ')
}
table.push([
item.ID,
entity,
title,
phones || '—',
emails || '—'
])
})
}
// Outputting the table to the console
console.table(table)
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Bitrix24\SDK\Services\CRM\Duplicates\Result\DuplicateResult;
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/');
// Requesting phone and email from the user
$phone = readline("Enter phone number: ");
$email = readline("Enter email: ");
// Initializing variables
$entityIDs = [
'LEAD' => [],
'CONTACT' => [],
'COMPANY' => []
];
$resultEntity = [
'lead' => [],
'contact' => [],
'company' => []
];
// Merges identifiers from the method response with the $entityIDs array
function mergeDuplicates(DuplicateResult $result, array &$entityIDs): void
{
$data = $result->getCoreResponse()->getResponseData()->getResult();
foreach (['LEAD', 'CONTACT', 'COMPANY'] as $type) {
if (!empty($data[$type]) && is_array($data[$type])) {
$entityIDs[$type] = array_merge($entityIDs[$type], $data[$type]);
}
}
}
// Searching for duplicates by phone
if($phone)
{
mergeDuplicates($sb->getCRMScope()->duplicate()->findByPhone([$phone]), $entityIDs);
}
// Searching for duplicates by email
if($email)
{
mergeDuplicates($sb->getCRMScope()->duplicate()->findByEmail([$email]), $entityIDs);
}
// Processing leads
if(!empty($entityIDs['LEAD']))
{
$leads = $sb->getCRMScope()->lead()->list(
[],
['ID' => $entityIDs['LEAD']],
['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL', 'TITLE']
)->getLeads();
if(!empty($leads))
{
$resultEntity['lead'] = $leads;
}
}
// Processing contacts
if(!empty($entityIDs['CONTACT']))
{
$contacts = $sb->getCRMScope()->contact()->list(
[],
['ID' => $entityIDs['CONTACT']],
['ID', 'NAME', 'LAST_NAME', 'PHONE', 'EMAIL'],
0
)->getContacts();
if(!empty($contacts))
{
$resultEntity['contact'] = $contacts;
}
}
// Processing companies
if(!empty($entityIDs['COMPANY']))
{
$companies = $sb->getCRMScope()->company()->list(
[],
['ID' => $entityIDs['COMPANY']],
['ID', 'PHONE', 'EMAIL', 'TITLE']
)->getCompanies();
if(!empty($companies))
{
$resultEntity['company'] = $companies;
}
}
// Forming the table
$table = [];
// Table header
$table[] = [
"Identifier",
"Object type",
"Name/First and last name",
"Phone",
"Email"
];
// Data rows
foreach ($resultEntity as $entityType => $entities) {
foreach ($entities as $item) {
$phones = '';
if (!empty($item->PHONE)) {
$phones = implode(', ', array_map(fn($phone) => $phone->VALUE, $item->PHONE));
}
$emails = '';
if (!empty($item->EMAIL)) {
$emails = implode(', ', array_map(fn($email) => $email->VALUE, $item->EMAIL));
}
$title = !empty($item->TITLE) ? $item->TITLE : '';
if (!empty($item->NAME) || !empty($item->LAST_NAME)) {
$namePart = trim($item->NAME . ' ' . $item->LAST_NAME);
if ($title) {
$title .= ': ' . $namePart;
} else {
$title = $namePart;
}
}
$table[] = [
$item->ID,
$entityType,
$title ?: '—',
$phones ?: '—',
$emails ?: '—'
];
}
}
// Outputting the table to the console using tabs
foreach ($table as $row) {
echo implode("\t", $row) . "\n";
}
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
phone = input("Enter phone number: ")
email = input("Enter email: ")
entity_ids = {"LEAD": [], "CONTACT": [], "COMPANY": []}
result_entity = {"lead": [], "contact": [], "company": []}
try:
if phone:
phone_result = client.crm.duplicate.findbycomm(
type="PHONE",
values=[phone],
).response.result
for key in entity_ids:
entity_ids[key].extend(phone_result.get(key, []))
if email:
email_result = client.crm.duplicate.findbycomm(
type="EMAIL",
values=[email],
).response.result
for key in entity_ids:
entity_ids[key].extend(email_result.get(key, []))
if entity_ids["LEAD"]:
result = client.crm.lead.list(
filter={"ID": entity_ids["LEAD"]},
select=["ID", "NAME", "LAST_NAME", "PHONE", "EMAIL", "TITLE"],
).response.result
if result:
result_entity["lead"] = result
if entity_ids["CONTACT"]:
result = client.crm.contact.list(
filter={"ID": entity_ids["CONTACT"]},
select=["ID", "NAME", "LAST_NAME", "PHONE", "EMAIL"],
).response.result
if result:
result_entity["contact"] = result
if entity_ids["COMPANY"]:
result = client.crm.company.list(
filter={"ID": entity_ids["COMPANY"]},
select=["ID", "PHONE", "EMAIL", "TITLE"],
).response.result
if result:
result_entity["company"] = result
except BitrixAPIError as error:
print(error)
table = [[
"Identifier",
"Object type",
"Name/First and last name",
"Phone",
"Email",
]]
for entity_type, entities in result_entity.items():
for item in entities:
phones = ""
if item.get("PHONE"):
phones = ", ".join(phone["VALUE"] for phone in item["PHONE"])
emails = ""
if item.get("EMAIL"):
emails = ", ".join(email["VALUE"] for email in item["EMAIL"])
title = item.get("TITLE") or ""
if item.get("NAME") or item.get("LAST_NAME"):
name_part = " ".join(filter(None, [item.get("NAME"), item.get("LAST_NAME")]))
if title:
title = f"{title}: {name_part}"
else:
title = name_part
table.append([
item["ID"],
entity_type,
title or "—",
phones or "—",
emails or "—",
])
for row in table:
print("\t".join(map(str, row)))
// Setup in an empty directory — go get will not work without go mod init:
//
// go mod init example && go get github.com/bitrix24/b24gosdk
//
// Run:
//
// export B24_WEBHOOK_URL='https://your-portal.bitrix24.com/rest/1/token/' && go run .
//
// The example is self-contained: it creates a lead, a contact, and a company with the same
// phone number and email, finds them as duplicates, prints the table, and cleans up after
// itself. It runs on any portal, nothing needs to be edited.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strings"
b24 "github.com/bitrix24/b24gosdk"
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// The webhook path is a secret, so it comes from the environment, not from the code.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// The phone and the email to search by. Neighbouring tabs ask them from
// the user; here they are set by constants, because the example itself creates
// objects with these values.
const (
phone = "+49 900 555-35-35"
email = "duplicate@example.com"
)
// The IDs of the found objects and their detailed data. The keys are
// the same ones that crm.duplicate.findbycomm returns.
entityIDs := map[string][]b24.ID{"LEAD": nil, "CONTACT": nil, "COMPANY": nil}
rows := make([]row, 0)
// --- setup: our own duplicates
cleanup, err := createDuplicates(ctx, core, phone, email)
defer cleanup()
if err != nil {
return err
}
// --- step 1: search for duplicates by communications
// The method searches by ONE communication type per call, so the phone and the email are
// queried separately, and the IDs are accumulated in a shared map.
for _, comm := range []struct{ typ, value string }{
{"PHONE", phone},
{"EMAIL", email},
} {
if comm.value == "" {
continue
}
res, err := core.Call(ctx, "crm.duplicate.findbycomm", b24.Params{
"type": comm.typ,
"values": []string{comm.value},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.duplicate.findbycomm %s: %w", comm.typ, err)
}
// The response is an object with the LEAD, CONTACT, and COMPANY keys. A key may be absent
// altogether: if nothing was found for that type, it is simply not sent.
var found map[string][]b24.ID
if err := json.Unmarshal(res.Result, &found); err != nil {
return fmt.Errorf("parse duplicates: %w", err)
}
for key := range entityIDs {
entityIDs[key] = appendUnique(entityIDs[key], found[key])
}
}
fmt.Printf("found: leads %d, contacts %d, companies %d\n",
len(entityIDs["LEAD"]), len(entityIDs["CONTACT"]), len(entityIDs["COMPANY"]))
// --- step 2: the lead data
if len(entityIDs["LEAD"]) > 0 {
res, err := core.Call(ctx, "crm.lead.list", b24.Params{
"filter": b24.Params{"ID": entityIDs["LEAD"]},
"select": []string{"ID", "NAME", "LAST_NAME", "PHONE", "EMAIL", "TITLE"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.lead.list: %w", err)
}
var leads []entity
if err := json.Unmarshal(res.Result, &leads); err != nil {
return fmt.Errorf("parse leads: %w", err)
}
for _, e := range leads {
rows = append(rows, e.row("lead"))
}
}
// --- step 3: the contact data
if len(entityIDs["CONTACT"]) > 0 {
res, err := core.Call(ctx, "crm.contact.list", b24.Params{
"filter": b24.Params{"ID": entityIDs["CONTACT"]},
"select": []string{"ID", "NAME", "LAST_NAME", "PHONE", "EMAIL"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.contact.list: %w", err)
}
var contacts []entity
if err := json.Unmarshal(res.Result, &contacts); err != nil {
return fmt.Errorf("parse contacts: %w", err)
}
for _, e := range contacts {
rows = append(rows, e.row("contact"))
}
}
// --- step 4: the company data
if len(entityIDs["COMPANY"]) > 0 {
// A company has no first or last name — its name is in TITLE.
res, err := core.Call(ctx, "crm.company.list", b24.Params{
"filter": b24.Params{"ID": entityIDs["COMPANY"]},
"select": []string{"ID", "TITLE", "PHONE", "EMAIL"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.company.list: %w", err)
}
var companies []entity
if err := json.Unmarshal(res.Result, &companies); err != nil {
return fmt.Errorf("parse companies: %w", err)
}
for _, e := range companies {
rows = append(rows, e.row("company"))
}
}
// --- print the table
if len(rows) == 0 {
fmt.Println("No duplicates found")
return nil
}
fmt.Println("ID\tType\tName\tPhone\tEmail")
for _, r := range rows {
fmt.Printf("%d\t%s\t%s\t%s\t%s\n", r.ID, r.Kind, r.Title, r.Phone, r.Email)
}
return nil
}
// entity is the common shape of a row in the lead, contact, and company lists: the field set
// differs between them, but the ones needed here match.
type entity struct {
ID b24.ID `json:"ID"`
Title string `json:"TITLE"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
Phone []multifield `json:"PHONE"`
Email []multifield `json:"EMAIL"`
}
// multifield is a row of a crm_multifield field: the phone and the email are stored
// as a list of objects, even when there is a single value.
type multifield struct {
Value string `json:"VALUE"`
}
type row struct {
ID b24.ID
Kind, Title, Phone, Email string
}
func (e entity) row(kind string) row {
title := strings.TrimSpace(e.Name + " " + e.LastName)
if title == "" {
title = e.Title
}
return row{ID: e.ID, Kind: kind, Title: title,
Phone: first(e.Phone), Email: first(e.Email)}
}
func first(values []multifield) string {
if len(values) == 0 {
return "—"
}
return values[0].Value
}
func appendUnique(dst, src []b24.ID) []b24.ID {
seen := make(map[b24.ID]bool, len(dst))
for _, id := range dst {
seen[id] = true
}
for _, id := range src {
if !seen[id] {
seen[id] = true
dst = append(dst, id)
}
}
return dst
}
// --- helpers: data setup and cleanup
// createDuplicates creates a lead, a contact, and a company with the same phone number and
// email — exactly the situation the scenario looks for. It returns a cleanup
// function: it is called even when the setup broke off halfway.
func createDuplicates(ctx context.Context, core *b24.Core, phone, email string) (func(), error) {
comm := b24.Params{
"PHONE": []map[string]any{b24.MultifieldAdd(phone, "WORK")},
"EMAIL": []map[string]any{b24.MultifieldAdd(email, "WORK")},
}
created := map[string]b24.ID{}
cleanup := func() {
for method, id := range created {
del(ctx, core, method, b24.Params{"id": id})
}
}
for _, spec := range []struct {
add, delete string
fields b24.Params
}{
{"crm.lead.add", "crm.lead.delete", b24.Params{"TITLE": "Filling out the CRM form \", "NAME": "Klaus", "LAST_NAME": "Weber"}},
{"crm.contact.add", "crm.contact.delete", b24.Params{"NAME": "Klaus", "LAST_NAME": "Weber"}},
{"crm.company.add", "crm.company.delete", b24.Params{"TITLE": "Filling out the CRM form \"}},
} {
fields := b24.Params{}
for k, v := range spec.fields {
fields[k] = v
}
for k, v := range comm {
fields[k] = v
}
res, err := core.Call(ctx, spec.add, b24.Params{"fields": fields})
if err != nil {
return cleanup, fmt.Errorf("%s: %w", spec.add, err)
}
var id b24.ID
if err := json.Unmarshal(res.Result, &id); err != nil {
return cleanup, fmt.Errorf("parse the ID from %s: %w", spec.add, err)
}
created[spec.delete] = id
}
return cleanup, nil
}
// del removes what was created. A cleanup error is printed but not returned: it must not
// mask the real error of the scenario.
func del(ctx context.Context, core *b24.Core, method string, params b24.Params) {
if _, err := core.Call(ctx, method, params); err != nil {
fmt.Fprintf(os.Stderr, "cleanup, %s: %v