Integration of 1C-Bitrix with the Nova Poshta delivery service (Ukraine)

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

1C-Bitrix Integration with Nova Poshta Delivery Service (Ukraine)

Nova Poshta is the dominant delivery service in the Ukrainian e-commerce market. Coverage spans virtually every locality in Ukraine, with its own branch network and courier delivery. The Nova Poshta API (version 2.0) is JSON-over-HTTP with API key authorization. Documentation is available in Ukrainian and Russian, and is well-structured.

Nova Poshta API Principles

All requests are POST to https://api.novaposhta.ua/v2.0/json/. Authorization is via the apiKey field in the body of each request. The request structure is uniform:

{
    "apiKey": "your_api_key",
    "modelName": "TrackingDocument",
    "calledMethod": "getStatusDocuments",
    "methodProperties": {}
}

This is an atypical REST architecture — a single endpoint for all methods, with the method passed as the calledMethod field. However, it wraps neatly into a single PHP method:

private function apiCall(string $model, string $method, array $props = []): array
{
    $payload = [
        'apiKey'            => $this->apiKey,
        'modelName'         => $model,
        'calledMethod'      => $method,
        'methodProperties'  => $props,
    ];

    $ch = curl_init('https://api.novaposhta.ua/v2.0/json/');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
    ]);

    $response = json_decode(curl_exec($ch), true);
    curl_close($ch);

    if (!$response['success']) {
        throw new \RuntimeException(implode(', ', $response['errors'] ?? ['Unknown error']));
    }

    return $response['data'];
}

Delivery Cost Calculation

public function calcCost(string $citySender, string $cityRecipient, float $weight): float
{
    $data = $this->apiCall('InternetDocument', 'getDocumentPrice', [
        'CitySender'         => $citySender,   // sender city ref
        'CityRecipient'      => $cityRecipient, // recipient city ref
        'ServiceType'        => 'WarehouseDoors', // branch-to-door
        'Weight'             => $weight,
        'Cost'               => 0, // declared value
        'CargoType'          => 'Cargo',
        'SeatsAmount'        => 1,
    ]);

    return (float)($data[0]['Cost'] ?? 0);
}

Delivery types (ServiceType): WarehouseWarehouse (branch-to-branch), WarehouseDoors (branch-to-door), DoorsDoors (door-to-door), DoorsWarehouse (door-to-branch). Pricing differs between types.

City and Branch Search by Keyword

public function findCity(string $query): array
{
    return $this->apiCall('Address', 'searchSettlements', [
        'CityName' => $query,
        'Limit'    => 5,
        'Page'     => 1,
    ]);
}

public function findWarehouse(string $cityRef, ?string $query = null): array
{
    return $this->apiCall('AddressGeneral', 'getWarehouses', [
        'CityRef'  => $cityRef,
        'FindByString' => $query,
        'Limit'    => 50,
    ]);
}

A dynamic search is implemented on the website: the customer enters a city name — searchSettlements is called; the customer selects a city — branches are loaded via getWarehouses. Results are cached for 1–4 hours.

Creating a Waybill (TTN)

public function createDocument(\Bitrix\Sale\Shipment $shipment): string
{
    $order = $shipment->getOrder();
    $props = $order->getPropertyCollection();

    $data = $this->apiCall('InternetDocument', 'save', [
        'PayerType'      => 'Recipient', // recipient pays
        'PaymentMethod'  => 'Cash',
        'DateTime'       => date('d.m.Y'),
        'CargoType'      => 'Cargo',
        'VolumeGeneral'  => 0.001,
        'Weight'         => max($this->getWeight($shipment), 0.1),
        'ServiceType'    => 'WarehouseWarehouse',
        'SeatsAmount'    => 1,
        'Description'    => 'Goods',
        'Cost'           => $order->getPrice(),

        'CitySender'     => $this->getOption('SENDER_CITY_REF'),
        'Sender'         => $this->getOption('SENDER_REF'),
        'SenderAddress'  => $this->getOption('SENDER_WAREHOUSE_REF'),
        'ContactSender'  => $this->getOption('CONTACT_REF'),
        'SendersPhone'   => $this->getOption('SENDER_PHONE'),

        'CityRecipient'  => $props->getItemByOrderPropertyCode('NP_CITY_REF')?->getValue(),
        'RecipientAddress' => $props->getItemByOrderPropertyCode('NP_WAREHOUSE_REF')?->getValue(),
        'RecipientsPhone'=> $props->getItemByOrderPropertyCode('PHONE')?->getValue(),
        'RecipientName'  => $props->getItemByOrderPropertyCode('FIO')?->getValue(),
        'Recipient'      => $this->createOrGetRecipient($props),
    ]);

    $intDocNumber = $data[0]['IntDocNumber'] ?? ''; // TTN
    $props->getItemByOrderPropertyCode('NP_TTN')?->setValue($intDocNumber);
    $order->save();

    return $intDocNumber;
}

The Sender, SenderAddress, and ContactSender parameters are ref identifiers of objects in the Nova Poshta system. They are obtained once during integration setup via the Counterparty/getCounterparties method.

TTN Tracking

public function trackDocument(string $ttn): array
{
    $data = $this->apiCall('TrackingDocument', 'getStatusDocuments', [
        'Documents' => [['DocumentNumber' => $ttn]],
    ]);

    $doc = $data[0] ?? [];
    return [
        'status'       => $doc['Status'] ?? '',
        'warehouseTo'  => $doc['WarehouseRecipient'] ?? '',
        'scheduledDate' => $doc['ScheduledDeliveryDate'] ?? '',
    ];
}

Nova Poshta does not provide push webhooks for status changes. Polling via agent every 2–3 hours for active TTNs.

Timelines

Scope Timeline
Calculation + city/branch search + widget 4–5 days
+ TTN creation + tracking +2 days
+ Auto-TTN creation on status change +1 day