Setting up customer data consolidation from different channels in 1C-Bitrix
One person leaves a trace in five places simultaneously: orders on the site, writes to Bitrix24 chat, calls through telephony, makes a purchase at checkout and subscribes to email list. In each channel — a separate identifier. Linking them into one profile without losses is a technical task that the standard sale module doesn't solve out of the box.
Data entry points and their storage locations
In a typical Bitrix installation, customer data is scattered across several independent tables:
-
Website:
b_user(registered),b_sale_fuser(anonymous),b_sale_order_props_value(contacts in order) -
Bitrix24 CRM:
b_crm_contact,b_crm_contact_phone,b_crm_contact_email— separate tables for multi-value fields -
Email lists:
b_subscribe_subscriberlinked toUSER_IDor just to an address without authorization -
Telephony:
b_voximplant_callwithCALLER_IDfield — phone number without link tob_user -
Offline checkout: data comes via 1C exchange into
b_iblock_element(if catalog is used) or into external table via custom module
Matching by identification keys
Consolidation is built on a chain of deterministic keys. Main ones: email, normalized phone number, cookie BITRIX_SM_SALE_UID (this is FUSER_ID), mobile device identifier from b_push_sender_subscription.
Phone normalization is a mandatory step. In b_crm_contact_phone numbers are stored as +7XXXXXXXXXX, in b_sale_order_props_value — in arbitrary format ("8 (495) 123-45-67"). For matching you need a function to convert to E.164:
function normalizePhone(string $phone): string {
$digits = preg_replace('/\D/', '', $phone);
if (strlen($digits) === 11 && $digits[0] === '8') {
$digits[0] = '7';
}
return '+' . $digits;
}
Identifier graph
An efficient consolidation architecture is a matching table (identity graph). At database level this is a separate table, for example bl_customer_identity:
CREATE TABLE bl_customer_identity (
id SERIAL PRIMARY KEY,
master_uid INT NOT NULL, -- ID of master profile in b_user
channel VARCHAR(50) NOT NULL, -- 'web', 'crm', 'voip', 'pos', 'email'
ext_id VARCHAR(255) NOT NULL, -- identifier in channel
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (channel, ext_id)
);
On each event in any channel (new order, call, subscription) the system searches for a record by (channel, ext_id). If found — links to existing master_uid. If not — checks for overlaps through other keys via b_crm_contact_phone and b_user.EMAIL, and only in complete absence creates a new master profile.
CRM module integration
Bitrix24 provides an API for finding duplicates: CCrmContact::GetList() with phone filter. The CCrmContactHelper::FindDuplicate() method searches for matches among contacts and leads. However, it only works within CRM and doesn't affect b_user.
For bidirectional linking: b_crm_contact has a field UF_CRM_WEB_USER_ID (custom field, needs to be created via CUserTypeEntity::Add()). Fill it on each successful match — and reverse search "contact → website user" becomes O(1) by index.
Conflict handling during merge
The most painful scenario: two profiles with different emails but one phone — and both with order history. You need a strategy for choosing the master record. A working approach: master = profile with the highest order sum in b_sale_order. All other records are aliases, their data is transferred to master, and the accounts are deactivated (ACTIVE = 'N' in b_user).
What we configure
- Audit of existing channels and data entry points in the system
- Creation of
bl_customer_identitytable and logic for its population - Function for phone and email normalization across all channels
- Event handlers:
OnSaleOrderSaved,OnCrmContactAdd,OnVoximplantCallEnd - API endpoint for mobile app passing device ID on authorization
- Administrative interface for manual conflict resolution during merge







