Bitrix24 Webhook Integration Development
The most common complaint about webhook integrations with Bitrix24 is: "it worked, then it stopped." The cause is almost always the same — the handler started responding slower than 5 seconds, Bitrix24 stopped calling it, and nobody noticed because there were no logs. Building a reliable webhook-based integration requires understanding how Bitrix24 delivers events and what happens during failures.
Webhook Integration Architecture
Webhooks in Bitrix24 work in two directions:
Outgoing webhook — Bitrix24 calls your URL when an event occurs. The subscription is configured in "Developers → Outgoing webhook" or via event.bind in the REST API. Bitrix24 makes a POST request with Content-Type: application/x-www-form-urlencoded.
Incoming webhook — your system calls Bitrix24 via a fixed URL with a token. Convenient for simple one-way integrations without OAuth.
For two-way integration, both types are typically used: incoming — to send data to Bitrix24, outgoing — to receive events.
Subscribing to Events via API
Programmatic subscription is more reliable than manual configuration in the interface — it does not depend on an administrator's actions:
// Subscribe via REST API (on behalf of an OAuth app)
$b24->call('event.bind', [
'event' => 'ONCRMDEALUPDATE',
'handler' => 'https://your-system.com/webhooks/bitrix24',
'auth_type' => 0, // 0 = current user
]);
// Get list of active subscriptions
$bindings = $b24->call('event.get');
// Unsubscribe
$b24->call('event.unbind', [
'event' => 'ONCRMDEALUPDATE',
'handler' => 'https://your-system.com/webhooks/bitrix24',
]);
Incoming Request Structure
Body of the POST request from Bitrix24:
event=ONCRMDEALUPDATE
&event_handler_id=42
&auth[access_token]=abc123
&auth[expires]=1700000000
&auth[member_id]=a1b2c3
&auth[domain]=company.bitrix24.ru
&auth[application_token]=xyz789
&data[FIELDS][ID]=456
&data[FIELDS][STAGE_ID]=EXECUTING
&data[FIELDS][OPPORTUNITY]=150000
Important: data[FIELDS] contains only the changed fields, not the full object. To get the current state of the deal, a separate call to crm.deal.get is needed.
auth[application_token] — token to verify the request source. For on-premise Bitrix24, verify it matches the application token.
Mandatory Pattern: Immediate Response + Queue
The key requirement: the handler must return HTTP 200 within 5 seconds. Anything longer — timeout, the event is considered undelivered.
// routes/api.php (Laravel)
Route::post('/webhooks/bitrix24', function (Request $request) {
// Token validation — fast
if (!validateBitrixToken($request->input('auth.application_token'))) {
return response('Forbidden', 403);
}
// Put in queue — fast
ProcessBitrixEvent::dispatch($request->all());
// Respond immediately
return response('OK', 200);
});
// app/Jobs/ProcessBitrixEvent.php
class ProcessBitrixEvent implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900]; // 1 min, 5 min, 15 min
public function handle(): void
{
$event = $this->payload['event'];
$dealId = $this->payload['data']['FIELDS']['ID'];
// Now retrieve the full object
$deal = $this->b24->call('crm.deal.get', ['id' => $dealId]);
// ... processing
}
}
Handler Idempotency
One event can arrive twice: Bitrix24 retries on network issues, and bulk operations generate ONCRMDEALUPDATE for every field. The handler must be idempotent:
// Deduplication via event_handler_id + timestamp
$eventKey = md5($event . $dealId . $request->input('ts'));
if ($redis->set("processed:{$eventKey}", 1, ['NX', 'EX' => 3600])) {
ProcessBitrixEvent::dispatch($payload);
// If the key already exists — duplicate, ignore
}
Event Chains and Synchronisation Loops
The classic problem: an external system receives an ONCRMDEALUPDATE event, updates data in its database, then sends the update back to Bitrix24 — this generates ONCRMDEALUPDATE again, and the cycle repeats indefinitely.
Solutions:
- Flag in the deal: set a custom field
UF_CRM_SYNC_LOCK=Ybefore writing from the external system, check it in the handler — ifY, skip and clear the flag - Data hash: compare the incoming data hash with the last processed one — if they match, skip
- Timestamp: if the event is about our own update (time ≤ 2 sec from our write) — ignore
Delivery Monitoring
Bitrix24 does not keep a delivery log for webhooks to external recipients. It must be maintained independently:
CREATE TABLE webhook_events (
id SERIAL PRIMARY KEY,
event_type VARCHAR(64),
entity_id INTEGER,
received_at TIMESTAMP DEFAULT NOW(),
processed_at TIMESTAMP,
status VARCHAR(16) DEFAULT 'pending', -- pending/done/error
error_message TEXT
);
Alert: if there is no ONCRMDEALUPDATE event for 10 minutes during business hours — Bitrix24 has most likely stopped calling the handler.
Event Limitations
| Event | Notes |
|---|---|
ONCRMDEALUPDATE |
Triggered on every change to any field |
ONCRMDEALADD |
Does not fire when importing via API with DISABLE_PORTAL_ACTIVITY=Y |
ONVOXIMPLANTCALLEND |
Call recording data is available with a 5–30 sec delay |
ONTASKUPDATE |
Does not include checklist changes |
ONIMBOTMESSAGEADD |
Only for bots registered via imbot.register |
On-Premise Bitrix24: Extended Events
On-premise installations provide PHP-level events that are not available in the REST API:
// Event before a deal is saved — data can be modified
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'crm', 'OnBeforeCrmDealAdd',
[MyHandler::class, 'onBeforeDealAdd']
);
// Custom event from a module
$event = new \Bitrix\Main\Event('my_module', 'OnSomethingHappened', ['data' => $data]);
$event->send();
Development Stages
| Stage | Content | Timeline |
|---|---|---|
| Design | List of events, data schema, handler architecture | 2–3 days |
| Endpoint and queue | HTTP handler, Job, deduplication | 3–5 days |
| Business logic | Processing each event type, calling external systems | 1–3 weeks |
| Loop protection | Sync flags, idempotency | 2–3 days |
| Monitoring | Event log, alerts, dashboard | 2–3 days |
| Testing | Event simulation, load tests | 3–5 days |
Total: 3–7 weeks depending on the number of events and complexity of business logic.







