Building a Custom Survey Widget on 1C-Bitrix: NPS, CSAT and More

When launching a new product or after changes in a service, the same picture often emerges: managers manually collect feedback via email or messengers, data gets lost, analysis lags behind. Recently, a client from an electronics online store came to us — their <cite>Net Promoter Score</cite> was unk

Our competencies:

Frequently Asked Questions

When launching a new product or after changes in a service, the same picture often emerges: managers manually collect feedback via email or messengers, data gets lost, analysis lags behind. Recently, a client from an electronics online store came to us — their Net Promoter Score was unknown because surveys were conducted once every six months via email with a 3% response rate. We placed a pop-up survey on the order page, and within a month we received 1200 responses with a detailed picture. In one month, NPS increased from 20 to 45 — this allowed us to quickly fix problematic areas in the delivery service. A survey on the site is the only way to get structured data without losses. Bitrix does not have a ready-made module for surveys of a decent level — the built-in iblock can be used for storage, but the survey logic has to be written from scratch.

Implementation of a survey on 1C-Bitrix

We build the architecture on HL-blocks — this is faster and more flexible than infoblocks. Typical scenarios:

  • NPS (Net Promoter Score) — 0–10 scale with comment.
  • CSAT (Customer Satisfaction) — 1–2 questions after a specific action.
  • Full survey — several pages with different question types, branching, display triggers.

Data structure via HL-blocks:

class SurveyTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName(): string { return 'b_hl_surveys'; } public static function getMap(): array { return [ new IntegerField('ID', ['primary' => true, 'autocomplete' => true]), new StringField('TITLE'), new StringField('SLUG'), new StringField('TYPE'), // nps | csat | full new TextField('QUESTIONS_JSON'), // Question configuration new StringField('TRIGGER'), // page_load | exit_intent | scroll_50 | after_order new IntegerField('SHOW_DELAY'), // Display delay in seconds new BooleanField('IS_ACTIVE', ['values' => [false, true]]), new DatetimeField('DATE_FROM'), new DatetimeField('DATE_TO'), ]; } } class SurveyResponseTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName(): string { return 'b_hl_survey_responses'; } public static function getMap(): array { return [ new IntegerField('ID', ['primary' => true, 'autocomplete' => true]), new IntegerField('SURVEY_ID'), new IntegerField('USER_ID'), // NULL — anonymous new StringField('SESSION_ID'), new StringField('USER_IP'), new TextField('ANSWERS_JSON'), // {question_id: answer} new IntegerField('COMPLETION_SEC'), // Completion time new DatetimeField('CREATED_AT'), ]; } } 

Survey types to implement

Choice depends on the business task. Compare options:

Type Essence Questions Trigger Implementation time
NPS Willingness to recommend 1–2 Pop-up on delay / exit 2–3 days
CSAT Specific point evaluation 1–3 After order / inquiry 2–4 days
Full Audience research 5–20 Combined 8–14 days

NPS survey — the simplest and cheapest way to start collecting feedback. It requires only one component with a widget.

NPS survey: simple case

Typical implementation — one page, one action. Component code:

// /local/components/local/survey.nps/template.php ?> <div class="nps-widget" id="nps-widget" style="display:none;"> <div class="nps-container"> <button class="nps-close" onclick="NPS.dismiss()">×</button> <p class="nps-question"> How likely are you to recommend us to your friends? </p> <div class="nps-scale"> <?php for ($i = 0; $i <= 10; $i++): ?> <button class="nps-score" data-score="<?= $i ?>"><?= $i ?></button> <?php endfor; ?> </div> <div class="nps-labels"> <span>Not at all likely</span> <span>Extremely likely</span> </div> <div class="nps-comment" style="display:none;"> <textarea placeholder="Tell us more..." id="nps-comment-text"></textarea> <button onclick="NPS.submit()">Submit</button> </div> </div> </div> <script> const NPS = { surveyId: <?= (int)$arResult['SURVEY']['ID'] ?>, selectedScore: null, init() { setTimeout(() => { if (!this.wasShown()) { document.getElementById('nps-widget').style.display = 'flex'; this.markShown(); } }, <?= (int)$arResult['SURVEY']['SHOW_DELAY'] * 1000 ?>); document.querySelectorAll('.nps-score').forEach(btn => { btn.addEventListener('click', e => { this.selectScore(parseInt(e.target.dataset.score)); }); }); }, selectScore(score) { this.selectedScore = score; document.querySelectorAll('.nps-score').forEach(b => b.classList.remove('selected')); document.querySelector(`[data-score="${score}"]`).classList.add('selected'); document.querySelector('.nps-comment').style.display = 'block'; }, async submit() { if (this.selectedScore === null) return; await fetch('/local/ajax/survey_submit.php', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ survey_id: this.surveyId, answers: { nps_score: this.selectedScore, comment: document.getElementById('nps-comment-text').value, }, sessid: BX.bitrix_sessid(), }), }); document.getElementById('nps-widget').innerHTML = '<p class="nps-thanks">Thank you for your feedback!</p>'; setTimeout(() => document.getElementById('nps-widget').style.display = 'none', 3000); }, dismiss() { document.getElementById('nps-widget').style.display = 'none'; this.markShown(); }, wasShown() { return !!localStorage.getItem('nps_shown_' + this.surveyId); }, markShown() { localStorage.setItem('nps_shown_' + this.surveyId, Date.now()); }, }; NPS.init(); </script> 

Display triggers

The survey should appear at the right moment, not immediately on load. Implement several triggers:

const SurveyTriggers = { exitIntent(callback) { document.addEventListener('mouseleave', e => { if (e.clientY <= 0) callback(); }, {once: true}); }, scrollDepth(percent, callback) { const listener = () => { const scrolled = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100; if (scrolled >= percent) { window.removeEventListener('scroll', listener); callback(); } }; window.addEventListener('scroll', listener); }, afterOrder(callback) { if (window.location.href.includes('/order/success/')) { setTimeout(callback, 2000); } }, }; 

Triggers are configured individually for each survey and can be combined. For example, a post-order survey Bitrix appears after a successful purchase, while an exit intent survey Bitrix triggers when leaving the page. To prevent duplicate responses, we use localStorage/cookie for anonymous users and check by user_id for authorized ones — this eliminates duplicates.

Analyzing responses

For NPS, automatically calculate the category of promoters, passives, and detractors:

class NpsAnalytics { public static function calculate(int $surveyId): array { $responses = SurveyResponseTable::getList([ 'filter' => ['SURVEY_ID' => $surveyId], 'select' => ['ANSWERS_JSON'], ])->fetchAll(); $detractors = 0; // low scores $passives = 0; // medium scores $promoters = 0; // high scores $total = 0; foreach ($responses as $r) { $answers = json_decode($r['ANSWERS_JSON'], true); $score = (int)($answers['nps_score'] ?? -1); if ($score < 0) continue; $total++; if ($score <= 6) $detractors++; elseif ($score <= 8) $passives++; else $promoters++; } if ($total === 0) return ['nps' => 0, 'total' => 0]; $nps = round(($promoters / $total - $detractors / $total) * 100); return [ 'nps' => $nps, 'total' => $total, 'promoters' => $promoters, 'passives' => $passives, 'detractors' => $detractors, ]; } } 

Step-by-step survey development process

  1. Analysis and agreement — define goals (NPS, CSAT, research), choose triggers and question types.
  2. HL-block design — create tables via migration API, configure fields.
  3. Component implementation — write widget templates, set up AJAX handlers.
  4. Trigger integration — add exit-intent, scroll, post-order events.
  5. Testing and debugging — test on different browsers, resolve conflicts.
  6. Launch and analytics — deliver dashboard with NPS, CSAT, conversion metrics.

Throughout this survey development Bitrix process, we ensure smooth Bitrix feedback collection.

What's included in the work

When you order a turnkey survey development, we provide:

  • HL-block setup and migrations
  • Component development (NPS, CSAT, custom)
  • Trigger implementation and duplicate protection
  • Analytics integration (NPS counter, summary reports)
  • API and configuration documentation
  • Access to repository and test environment
  • 30 days of technical support after launch

Why HL-blocks are better than infoblocks?

Highload-blocks are tables with an arbitrary set of fields. Infoblocks are designed for content filling, while HL-blocks are for fast queries with minimal load. For surveys, write speed without extra events is critical. Comparison table:

Criterion Infoblocks HL-blocks
Write performance Medium (events, overhead) High (direct INSERT)
Field flexibility Fixed types Arbitrary fields via ORM
JSON field support No Yes, TextField
Suitable for surveys Only result storage Configuration and response storage

Timelines and conditions

Timelines depend on complexity: simple widget — from 2 days, full survey — up to 14 days. Cost is calculated individually, but typical prices start from $500 for a simple NPS widget and up to $2000 for a full survey with branching and multiple triggers. Payback occurs within the first month due to automated data collection and timely problem identification. We rely on HL-blocks — a standard Bitrix tool that guarantees compatibility with updates. Our experience of 10+ years and more than 50 implemented projects confirms the stability of solutions. Get a consultation for your task — we will estimate the scope without rigid deadlines. Order survey development — and within a month you will see the clean NPS of your business.