Setting up supplier inventory synchronization for 1C-Bitrix dropshipping

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
    1173
  • 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
    745
  • 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

Configuring Supplier Stock Synchronisation for Dropshipping in 1C-Bitrix

A customer places an order for a product that the supplier sold out long ago — the classic dropshipping problem. To prevent this, stock levels on the site must reflect real supplier availability. Bitrix stores stock in b_catalog_store_product; the task is to keep that table current.

Synchronisation Architecture

Three options for receiving data from a supplier:

Push webhook from the supplier — the supplier notifies us whenever stock changes. The fastest option; requires the supplier to be technically capable of sending POST requests.

Scheduled pull feed — a Bitrix agent downloads a file (XML/CSV/JSON) from the supplier's FTP or HTTP endpoint every N minutes. The most common approach.

Direct access to the supplier's API — we query stock at the moment a product page is viewed or an item is added to the cart. Puts load on the supplier's API, but delivers accurate data.

Pull Synchronisation via Feed

A Bitrix agent runs on a schedule and updates stock levels:

// /local/lib/Dropshipping/StockSync/FeedProcessor.php
namespace Local\Dropshipping\StockSync;

class FeedProcessor
{
    public function syncSupplier(int $supplierId): SyncResult
    {
        $supplier = SupplierRepository::findById($supplierId);
        $raw      = $this->downloadFeed($supplier['UF_FEED_URL'], $supplier['UF_API_KEY']);
        $items    = $this->parseFeed($raw, $supplier['UF_FEED_FORMAT']);

        $updated = 0;
        $skipped = 0;

        foreach ($items as $item) {
            $productId = SupplierProductMap::findProductId(
                $supplierId,
                $item['sku']
            );

            if (!$productId) {
                $skipped++;
                continue;
            }

            $this->updateStock($productId, $supplier['UF_STORE_ID'], (int)$item['quantity']);
            $updated++;
        }

        return new SyncResult($supplierId, $updated, $skipped);
    }

    private function updateStock(int $productId, int $storeId, int $quantity): void
    {
        $existing = \CCatalogStoreProduct::GetList(
            [],
            ['PRODUCT_ID' => $productId, 'STORE_ID' => $storeId]
        )->Fetch();

        if ($existing) {
            \CCatalogStoreProduct::Update($existing['ID'], ['AMOUNT' => $quantity]);
        } else {
            \CCatalogStoreProduct::Add([
                'PRODUCT_ID' => $productId,
                'STORE_ID'   => $storeId,
                'AMOUNT'     => $quantity,
            ]);
        }

        // Update total stock in b_catalog_product
        $total = $this->getTotalStock($productId);
        \CCatalogProduct::Update($productId, ['QUANTITY' => $total]);
    }
}

Parsing Feed Formats

Suppliers use different formats. We implement an interface and concrete parsers:

interface FeedParserInterface
{
    public function parse(string $raw): array; // returns [{sku, quantity}, ...]
}

class CsvFeedParser implements FeedParserInterface
{
    public function parse(string $raw): array
    {
        $lines  = str_getcsv($raw, "\n");
        $result = [];

        // Detect delimiter: ; or ,
        $delimiter = str_contains($lines[0], ';') ? ';' : ',';

        foreach (array_slice($lines, 1) as $line) { // skip header
            [$sku, $qty] = str_getcsv($line, $delimiter);
            if ($sku && is_numeric($qty)) {
                $result[] = ['sku' => trim($sku), 'quantity' => (int)$qty];
            }
        }

        return $result;
    }
}

class XmlFeedParser implements FeedParserInterface
{
    public function parse(string $raw): array
    {
        $xml    = simplexml_load_string($raw, 'SimpleXMLElement', LIBXML_NOCDATA);
        $result = [];

        foreach ($xml->offer as $offer) {
            $result[] = [
                'sku'      => (string)$offer->vendorCode,
                'quantity' => (int)$offer->stock,
            ];
        }

        return $result;
    }
}

Push Synchronisation (Supplier Webhook)

// /local/ajax/supplier/stock-webhook.php
\Bitrix\Main\Application::getInstance()->initializeExtended();

$key        = $_SERVER['HTTP_X_SUPPLIER_KEY'] ?? '';
$supplierId = \Local\Dropshipping\SupplierAuth::validate($key);

if (!$supplierId) {
    http_response_code(403);
    die('{"error":"Unauthorized"}');
}

$body  = json_decode(file_get_contents('php://input'), true);
$items = $body['items'] ?? [];

$processor = new \Local\Dropshipping\StockSync\FeedProcessor();
$result    = $processor->syncItems($supplierId, $items);

echo json_encode(['updated' => $result->updated, 'skipped' => $result->skipped]);

Registering the Sync Agent

// Agent runs every 30 minutes for each supplier
\CAgent::Add([
    'NAME'       => '\Local\Dropshipping\StockSync\SyncAgent::run();',
    'MODULE_ID'  => 'local',
    'PERIOD'     => 1800, // 30 minutes
    'NEXT_EXEC'  => date('d.m.Y H:i:s', time() + 1800),
    'ACTIVE'     => 'Y',
]);

Handling Zero-Stock Products

When stock reaches zero, it is important not only to set QUANTITY = 0 but also to delist the product or display "Out of stock":

private function handleZeroStock(int $productId): void
{
    // Set "Out of stock" status via b_catalog_product
    \CCatalogProduct::Update($productId, [
        'QUANTITY'             => 0,
        'QUANTITY_TRACE'       => 'Y',
        'CAN_BUY_ZERO'        => 'N',
        'NEGATIVE_AMOUNT_TRACE' => 'N',
    ]);

    // Invalidate the product page cache
    \Bitrix\Main\Data\TaggedCache::clearByTag('iblock_id_' . CATALOG_IBLOCK_ID);
}

Logging and Monitoring

Every synchronisation run is logged to the StockSyncLog HL-block table:

Field Value
UF_SUPPLIER_ID Supplier ID
UF_DATE Synchronisation date
UF_UPDATED Number of updated items
UF_SKIPPED Unmatched SKUs
UF_DURATION Execution time, ms
UF_ERROR Error message (if any)

If no successful synchronisation has occurred for an active supplier in the past 2 hours, a monitor agent sends an alert to the technical administrator's email.