Custom Callback Form: from Validation to Auto-Dialing
The standard bitrix:main.feedback component doesn't validate phone numbers, protect against spam, or create leads. Managers waste up to 30% of their time manually processing requests. A custom callback form solves these issues: client-side and server-side phone validation, rate limiter via cache, automatic lead creation with UTM tags, and task assignment. With a load of 1000+ requests per day, the form remains stable thanks to tagged caching and Bitrix 2.0 component architecture. Our experience – 50+ implementations for online stores and service companies, average request processing time cut by 4 times compared to the standard form. Over 5 years in Bitrix development, we have delivered more than 100 projects, reducing call abandonment by 70% for our clients.
The key difference is a built-in orchestrator: the form analyzes operator work hours, distributes leads to the least busy managers, and (optionally) initiates a callback via telephony API. All this using ready-made Bitrix 2.0 components with minimal core dependency.
Why the Standard Form Falls Short
The standard bitrix:main.feedback lacks built-in phone validation, anti-spam, and CRM binding. This leads to up to 40% of requests being lost: customers enter wrong numbers, bots clutter the system, and managers spend hours on manual entry. A custom form addresses these issues: it checks the phone against the +7 mask, limits request frequency via rate limiter, and automatically creates a lead with UTM tags.
How We Protect the Form from Spam
We use three-level protection: CSRF token in each request, rate limiter based on tagged cache (max 2 requests per IP per hour), and client-side validation before submission. Optionally, we can add reCAPTCHA. As a result, spam requests drop by 95%.
What Problems the Custom Form Solves
- No phone validation – the customer enters anything, the manager wastes time clarifying. Our form checks length, +7 mask, and blocks invalid numbers both client-side and server-side.
- Spam bots fill the CRM – without rate limiter and CSRF protection, leads arrive in batches. We set a limit of 2 requests per IP per hour and check the session.
- No CRM binding – the lead is not created automatically; the manager enters data manually. We create a lead with phone, name, UTM tags, and immediately set a call task.
- No auto-dialing – the customer waits for a call for hours. If operators are working, the system itself initiates a call via telephony API.
- No schedule awareness – requests outside working hours get lost. We configure a calendar: on weekends, deferred messages with the next call time.
How We Implement the Callback Form
Solution Architecture
Client JavaScript sends an AJAX request to /local/api/callback.php. The server validates the phone, checks anti-spam, creates a lead in Bitrix24 CRM, and (optionally) initiates a call via telephony API. If it's non-working hours, the lead is created and the call is deferred until the next working hour.
Server Handler
// /local/api/callback.php require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php'); header('Content-Type: application/json'); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit(json_encode(['success' => false, 'error' => 'Method not allowed'])); } $data = json_decode(file_get_contents('php://input'), true); // CSRF check $csrfToken = $data['sessid'] ?? ''; if (!\bitrix_sessid_check($csrfToken)) { http_response_code(403); exit(json_encode(['success' => false, 'error' => 'Invalid session'])); } $phone = preg_replace('/\D/', '', $data['phone'] ?? ''); // Phone validation if (!preg_match('/^[78]\d{10}$/', $phone)) { exit(json_encode(['success' => false, 'error' => 'Invalid phone format'])); } $phone = '+7' . substr($phone, -10); // Anti-spam: max 2 requests per IP per hour $limiter = new \Local\Callback\RateLimiter(); if (!$limiter->allow($_SERVER['REMOTE_ADDR'])) { exit(json_encode(['success' => false, 'error' => 'Too many requests. Try later.'])); } // Create lead in CRM $leadCreator = new \Local\Callback\LeadCreator(); $leadId = $leadCreator->create([ 'phone' => $phone, 'name' => htmlspecialchars(mb_substr($data['name'] ?? '', 0, 100)), 'comment' => htmlspecialchars(mb_substr($data['comment'] ?? '', 0, 500)), 'source' => $data['source'] ?? 'callback_form', 'page' => $_SERVER['HTTP_REFERER'] ?? '', 'utm' => $data['utm'] ?? [], ]); // Initiate callback if during working hours $scheduler = new \Local\Callback\WorkSchedule(); if ($scheduler->isWorkingNow()) { (new \Local\Callback\AutoDialer())->initiate($phone, $leadId); $message = 'We will call you back within 2 minutes'; } else { $message = 'We will call you back during next working hours: ' . $scheduler->getNextWorkStart(); } exit(json_encode(['success' => true, 'message' => $message, 'lead_id' => $leadId])); Creating a Lead in CRM
namespace Local\Callback; class LeadCreator { public function create(array $data): int { $fields = [ 'TITLE' => 'Callback: ' . $data['phone'], 'NAME' => $data['name'] ?: 'Client', 'PHONE' => [['VALUE' => $data['phone'], 'VALUE_TYPE' => 'WORK']], 'SOURCE_ID' => 'CALLBACK', 'STATUS_ID' => 'NEW', 'ASSIGNED_BY_ID' => $this->getAvailableManager(), 'COMMENTS' => $this->buildComment($data), 'UF_UTM_SOURCE' => $data['utm']['utm_source'] ?? '', 'UF_UTM_CAMPAIGN'=> $data['utm']['utm_campaign'] ?? '', 'UF_CALLBACK_PAGE' => mb_substr($data['page'] ?? '', 0, 255), ]; $lead = new \CCrmLead(false); $leadId = $lead->Add($fields, true); if ($leadId) { // Add task for manager: call back $this->addCallTask($leadId, $data['phone'], $fields['ASSIGNED_BY_ID']); } return (int)$leadId; } private function addCallTask(int $leadId, string $phone, int $assigneeId): void { \CCrmActivity::Add([ 'TYPE_ID' => \CCrmActivityType::Call, 'SUBJECT' => 'Call back: ' . $phone, 'OWNER_TYPE_ID' => \CCrmOwnerType::Lead, 'OWNER_ID' => $leadId, 'RESPONSIBLE_ID' => $assigneeId, 'DEADLINE' => (new \Bitrix\Main\Type\DateTime())->add('+1H'), 'COMPLETED' => 'N', ]); } private function getAvailableManager(): int { // Round-robin: select manager with fewest open leads $managers = [5, 7, 12, 15]; // Employee IDs $counts = []; foreach ($managers as $id) { $res = \CCrmLead::GetList( [], ['ASSIGNED_BY_ID' => $id, 'STATUS_ID' => 'NEW'], ['COUNT' => true] ); $counts[$id] = (int)$res; } asort($counts); return array_key_first($counts); } } The method CCrmLead::Add is described in the 1C-Bitrix documentation.
Work Schedule and Time Management
namespace Local\Callback; class WorkSchedule { private array $schedule = [ 1 => ['09:00', '19:00'], // Mon 2 => ['09:00', '19:00'], // Tue 3 => ['09:00', '19:00'], // Wed 4 => ['09:00', '19:00'], // Thu 5 => ['09:00', '19:00'], // Fri 6 => ['10:00', '16:00'], // Sat 0 => null, // Sun — day off ]; public function isWorkingNow(): bool { $tz = new \DateTimeZone('Europe/Moscow'); $now = new \DateTime('now', $tz); $dow = (int)$now->format('w'); // 0=Sun $hours = $this->schedule[$dow] ?? null; if (!$hours) return false; $start = \DateTime::createFromFormat('H:i', $hours[0], $tz); $end = \DateTime::createFromFormat('H:i', $hours[1], $tz); return $now >= $start && $now < $end; } public function getNextWorkStart(): string { $tz = new \DateTimeZone('Europe/Moscow'); $now = new \DateTime('now', $tz); for ($i = 1; $i <= 7; $i++) { $next = clone $now; $next->modify("+{$i} day"); $dow = (int)$next->format('w'); $hours = $this->schedule[$dow] ?? null; if ($hours) { $next->setTime(...explode(':', $hours[0])); return $next->format('d.m at H:i'); } } return 'Monday'; } } Rate Limiter via Bitrix Cache
namespace Local\Callback; class RateLimiter { private const MAX_ATTEMPTS = 2; private const WINDOW_SECONDS = 3600; public function allow(string $identifier): bool { $key = 'callback_rl_' . md5($identifier); $cache = \Bitrix\Main\Application::getInstance()->getManagedCache(); $count = (int)$cache->get($key); if ($count >= self::MAX_ATTEMPTS) { return false; } $cache->set($key, $count + 1, self::WINDOW_SECONDS); return true; } } Client-Side Form with Mask and AJAX
(function () { const form = document.getElementById('callback-form'); if (!form) return; const phoneInput = form.querySelector('[name="phone"]'); // Phone input mask phoneInput.addEventListener('input', function () { let val = this.value.replace(/\D/g, ''); if (val.startsWith('8') || val.startsWith('7')) val = val.slice(1); val = val.slice(0, 10); let formatted = '+7 '; if (val.length > 0) formatted += '(' + val.slice(0, 3); if (val.length >= 3) formatted += ') ' + val.slice(3, 6); if (val.length >= 6) formatted += '-' + val.slice(6, 8); if (val.length >= 8) formatted += '-' + val.slice(8, 10); this.value = formatted; }); form.addEventListener('submit', async function (e) { e.preventDefault(); const submitBtn = form.querySelector('[type="submit"]'); submitBtn.disabled = true; const phone = phoneInput.value.replace(/\D/g, ''); if (phone.length < 11) { showError('Please enter a valid phone number'); submitBtn.disabled = false; return; } const payload = { phone : phone, name : form.querySelector('[name="name"]')?.value || '', sessid : BX.bitrix_sessid(), utm : getUtmParams(), }; try { const res = await fetch('/local/api/callback.php', { method : 'POST', headers : { 'Content-Type': 'application/json' }, body : JSON.stringify(payload), }); const data = await res.json(); if (data.success) { showSuccess(data.message); form.reset(); } else { showError(data.error || 'An error occurred'); } } catch { showError('Connection error. Please try again.'); } submitBtn.disabled = false; }); function getUtmParams() { const params = new URLSearchParams(window.location.search); return { utm_source : params.get('utm_source') || getCookie('utm_source') || '', utm_campaign : params.get('utm_campaign') || getCookie('utm_campaign') || '', }; } })(); Comparison: Standard vs Custom Form
| Parameter | Standard bitrix:main.feedback | Custom Turnkey Form |
|---|---|---|
| Phone validation | Server-side only, no mask | Client + server, auto-format +7 (***) *-- |
| Anti-spam | None | Rate limiter + CSRF token |
| CRM integration | No, data only to email | Lead creation + call task + manager rotation |
| Working hours | Not considered | Deferred notifications and auto-dialing by schedule |
| UTM tags | Not passed | Stored in lead and session |
Typical Mistakes When Developing a Callback Form
| Mistake | Solution |
|---|---|
| Missing rate limiter | Use tagged cache with TTL – up to 2 requests per IP per hour |
| No CSRF check | Add bitrix_sessid_check() to every POST request |
| Inflexible schedule | Make settings in admin interface with timezone support |
| Ignoring UTM | Save UTM tags in lead and session for traffic source analysis |
Work Schedule Configuration
The schedule is defined in the $schedule array in the WorkSchedule class. You can edit directly in code or export settings to a high-load block. Any number of weekdays is supported, time in 'H:i' format.
Development Process
- Analysis – We examine your current form, load, operator schedule, and telephony provider.
- Design – We agree on layout, logic, and component architecture.
- Implementation – We write the component, server API, CRM integration, and telephony connection.
- Testing – We check validation, anti-spam, auto-dialing, and non-working hours behavior.
- Deployment – We roll out to production and set up error monitoring.
What's Included in the Work
- Custom component
local:callback.formwith templates (popup, inline form, floating button) - Server handler with CSRF, rate limiting, validation
- Lead creation in CRM, task for manager, responsible rotation
- Work schedule with timezone support
- JS: phone mask, AJAX submission, UTM passing
- Email/SMS notification on new request
- (Optional) Auto-dialing via telephony API
- Component and settings documentation
- 12-month code warranty
Timeline and Pricing
Our basic callback form with CRM integration starts at $1,500 (1–2 weeks). Full functionality with auto-dialing, scheduling, and analytics – from $3,000 (3–4 weeks). This investment typically saves clients $2,000/month in manual processing. Pricing is determined individually after analyzing your requirements.
A custom callback form processes requests 3 times faster than a standard one, and call conversion increases by 60%. Contact us for a consultation – we will evaluate your project within one business day.







