Setting Up Shipment Tracking in 1C-Bitrix
Shipment tracking is the automatic retrieval and display of delivery statuses from the delivery service. The customer sees the current status in their personal account; the manager sees it in the admin panel without having to log into each delivery service's own portal.
Two approaches to tracking
Webhooks (push): the delivery service itself sends a POST request when the status changes. Does not load the server, data is real-time. Requires a white IP or public domain. Supported by: CDEK, Yandex Delivery, Dostavista.
Polling (pull): a 1C-Bitrix agent periodically queries the delivery service API. Works without a white IP but creates load with a large number of active shipments. Used for Boxberry, PEK, Delovye Linii, Russian Post.
Polling agent
class DeliveryTrackingAgent
{
public static function run(): string
{
$activeShipments = self::getActiveShipments();
foreach ($activeShipments as $shipment) {
try {
$service = \Bitrix\Sale\Delivery\Services\Manager::getObjectById(
$shipment['DELIVERY_ID']
);
$trackCode = $shipment['TRACKING_CODE'];
$newStatus = $service->getTrackingStatus($trackCode);
if ($newStatus !== $shipment['LAST_STATUS']) {
self::updateStatus($shipment['ORDER_ID'], $newStatus);
self::notifyIfNeeded($shipment['ORDER_ID'], $newStatus);
}
} catch (\Exception $e) {
\Bitrix\Main\Diag\Debug::writeToFile($e->getMessage(), 'tracking');
}
}
return __CLASS__ . '::run();';
}
private static function getActiveShipments(): array
{
// Select orders with statuses where tracking is still relevant
$finalStatuses = ['DELIVERED', 'RETURNED', 'CANCELLED'];
// Return orders not in final statuses that have a tracking code
return \Bitrix\Sale\Internals\ShipmentTable::getList([
'filter' => [
'!TRACKING_NUMBER' => false,
'!STATUS_ID' => $finalStatuses,
],
'select' => ['ORDER_ID', 'DELIVERY_ID', 'TRACKING_NUMBER', 'TRACKING_STATUS'],
])->fetchAll();
}
}
The agent is registered during module installation via \CAgent::AddAgent('DeliveryTrackingAgent::run();', ...) with an interval of 3600 seconds (once per hour).
Webhook handler
// /local/php_interface/delivery_webhook.php
$data = json_decode(file_get_contents('php://input'), true);
$source = $_GET['source'] ?? '';
$handler = DeliveryWebhookHandlerFactory::create($source);
$handler->process($data);
A single URL (/bitrix/services/delivery/webhook.php?source=sdek) receives webhooks from all services. The source parameter determines which handler to use.
Displaying tracking in the customer account
In the bitrix:sale.order.detail component template, add a tracking block:
// In the detail/template.php template
if ($trackCode = $arResult['ORDER']['PROPS']['TRACKING_CODE']['VALUE']) {
$events = TrackingCache::get($trackCode); // cache for 30 minutes
include __DIR__ . '/tracking_timeline.php';
}
The tracking_timeline.php template displays the status history as a visual timeline — the current step is highlighted.
Tracking data is cached in \Bitrix\Main\Data\Cache for 30–60 minutes: a parcel's status does not change every second, and without caching every order page open generates an API request to the external service.
Storing status history
Each status change is saved to a separate table b_sale_order_delivery_status_log (custom) or to order notes (OnSaleOrderHistoryInsert). This allows the full event timeline to be shown to the customer, not just the current status.
private static function updateStatus(int $orderId, string $newStatus): void
{
// Save to status history
\Bitrix\Main\Application::getConnection()->add(
'b_delivery_status_log',
[
'ORDER_ID' => $orderId,
'STATUS' => $newStatus,
'DATE_ADD' => new \Bitrix\Main\Type\DateTime(),
]
);
// Update current status in the order property
$order = \Bitrix\Sale\Order::load($orderId);
$order->getPropertyCollection()
->getItemByOrderPropertyCode('TRACKING_STATUS')
?->setValue($newStatus);
$order->save();
}
Notifications on status change
private static function notifyIfNeeded(int $orderId, string $newStatus): void
{
$notifyStatuses = ['arrived_at_destination', 'out_for_delivery', 'delivered'];
if (!in_array($newStatus, $notifyStatuses)) return;
$event = new \Bitrix\Main\Mail\Event([
'EVENT_NAME' => 'DELIVERY_STATUS_CHANGED',
'LID' => SITE_ID,
'C_FIELDS' => [
'ORDER_ID' => $orderId,
'STATUS' => $newStatus,
],
]);
$event->send();
}
Polling performance at scale
With 500+ active shipments, polling once per hour generates 500 external API requests per agent run. This loads both your server and the delivery service API (most have rate limits). Optimizations:
- Batch requests: Russian Post and some other services support requesting statuses for multiple tracking numbers in a single request — use it.
- Prioritization: first poll shipments in active statuses ("Courier en route", "Arrived in city"), final statuses — less frequently.
- Throttling: insert
usleep(200000)(200 ms) between requests — avoid overloading the API.
Implementation timelines
Setting up a polling agent for one delivery service — 1 working day. Webhook handler — 1 day. Displaying tracking in the customer account with status history and notifications — 2–3 days in total.







