Linking Ad Spend to Bitrix24 CRM
Ad spend lives in Yandex Direct, Google Ads, VKontakte, myTarget — each in its own dashboard with its own metrics. Revenue lives in Bitrix24 CRM. Without connecting the two, it is impossible to calculate real ROI by channel: how much was spent on Direct and how much of that closed into money. Setting up this link is a task at the intersection of advertising APIs and CRM.
Attribution principle
The connection "ad → lead → deal → revenue" is built on UTM tags. Each ad is tagged with UTM parameters that are captured in Bitrix24 when a lead is created. After a deal closes, the revenue from that deal is attributed to the campaign.
utm_source=yandex&utm_medium=cpc&utm_campaign=brand&utm_term=buy
→ Lead (UF_CRM_UTM_*) → Deal → Amount
+ Campaign spend from Yandex Direct API
= ROI
Custom field structure
Deals in Bitrix24 need fields to store UTM data:
| Field | Code | Type |
|---|---|---|
| Source | UF_CRM_UTM_SOURCE | string |
| Campaign | UF_CRM_UTM_CAMPAIGN | string |
| Campaign ID | UF_CRM_CAMPAIGN_ID | string |
| Medium | UF_CRM_UTM_MEDIUM | string |
| Keyword | UF_CRM_UTM_TERM | string |
| Ad ID | UF_CRM_AD_ID | string |
Fields are created via crm.userfield.add or through the Bitrix24 interface.
When a lead is created from any advertising channel (Direct, VK, Facebook) — UTM tags are saved to these fields. When a lead is converted to a deal, the fields are copied (configured via "Sources" in Bitrix24 or via REST).
Importing ad spend from advertising platforms
A daily cron job imports the previous day's spend from each advertising platform into an aggregation table:
// Yandex Direct: campaign spend for the day
public function importDirectCosts(string $date): void
{
$report = $this->directApi->getReport([
'ReportType' => 'CAMPAIGN_PERFORMANCE_REPORT',
'DateRangeType' => 'CUSTOM_DATE',
'DateFrom' => $date,
'DateTo' => $date,
'FieldNames' => ['CampaignId', 'CampaignName', 'Cost', 'Clicks', 'Impressions'],
]);
foreach ($report as $row) {
AdCostTable::add([
'DATE' => $date,
'SOURCE' => 'yandex',
'CAMPAIGN_ID' => $row['CampaignId'],
'CAMPAIGN' => $row['CampaignName'],
'COST' => $row['Cost'] / 1000000, // Direct returns values in microroubles
'CLICKS' => $row['Clicks'],
]);
}
}
The same approach applies to VK Ads (ads.getStatistics) and Google Ads (Google Ads API, CampaignService).
ROI report in Bitrix24
The final report is built with a query joining spend and revenue:
SELECT
d.UF_CRM_UTM_SOURCE as source,
d.UF_CRM_CAMPAIGN_ID as campaign_id,
SUM(ac.COST) as ad_spend,
COUNT(DISTINCT d.ID) as deals_count,
SUM(d.OPPORTUNITY) as revenue,
ROUND((SUM(d.OPPORTUNITY) - SUM(ac.COST)) / NULLIF(SUM(ac.COST), 0) * 100, 1) as roi_pct
FROM b_crm_deal d
LEFT JOIN local_ad_costs ac
ON ac.CAMPAIGN_ID = d.UF_CRM_CAMPAIGN_ID
AND ac.SOURCE = d.UF_CRM_UTM_SOURCE
AND DATE(ac.DATE) = DATE(d.DATE_CREATE)
WHERE d.STAGE_ID = 'WON'
AND d.DATE_CLOSE BETWEEN :date_from AND :date_to
GROUP BY d.UF_CRM_UTM_SOURCE, d.UF_CRM_CAMPAIGN_ID
ORDER BY roi_pct DESC
The report is displayed in the Bitrix24 admin area or in an integrated BI tool (Yandex DataLens, Google Looker Studio).
Pushing spend back to Yandex Metrica
To display ROI directly in Metrica — upload spend via counter.uploadOfflineConversions (deprecated) or through the Data2 API. When end-to-end analytics runs through Metrica — connect via "Import spend data" from a CSV file generated by 1C-Bitrix.
Scope of work
- Creating custom fields in Bitrix24 deals
- Configuring UTM pass-through on lead creation (all channels)
- Developing spend importers: Direct, VK Ads, Facebook/Instagram
-
local_ad_costsaggregation table - ROI report by campaign with period and channel filters
- Cron schedule for import
Timeline: 1–2 weeks for a basic setup (Direct + one channel). 3–4 weeks for a multi-channel ROI report with a BI dashboard.







