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.







