Setting up shipment tracking in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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.