Dropshipping Architecture with Suppliers in 1C-Bitrix
Dropshipping on Bitrix is not a ready-made module — it is an architectural decision. A poorly designed schema breaks when a second supplier is added or when order volume reaches 100+ per day. A well-designed one handles 50 suppliers and a thousand orders per day without manual intervention.
Data Model
The central question: how to associate a product with a supplier, and how to store data for order routing.
Product infoblock (b_iblock_element, b_iblock_element_property) stores retail data: name, description, images, attributes. Leave this untouched.
HL-block Supplier — supplier directory:
b_uts_supplier (auto-generated HL-block table)
├── ID
├── UF_NAME — supplier name
├── UF_EMAIL — email for notifications
├── UF_WEBHOOK_URL — URL for POST notifications
├── UF_API_KEY — supplier API access key
├── UF_FEED_URL — stock feed URL (XML/CSV/JSON)
├── UF_FEED_FORMAT — feed format
├── UF_LEAD_TIME — order processing time (days)
└── UF_ACTIVE — active flag
HL-block SupplierProduct — product-to-supplier mapping:
b_uts_supplier_product
├── ID
├── UF_PRODUCT_ID — infoblock element ID (b_iblock_element.ID)
├── UF_SUPPLIER_ID — supplier ID (b_uts_supplier.ID)
├── UF_SUPPLIER_SKU — supplier SKU
├── UF_PURCHASE_PRICE — purchase price
├── UF_CURRENCY — purchase currency
├── UF_STORE_ID — supplier warehouse (b_catalog_store.ID)
├── UF_MIN_QUANTITY — minimum order quantity
└── UF_IS_PRIMARY — primary supplier (if multiple)
Warehouses (b_catalog_store) — one per supplier. Stock levels are stored in b_catalog_store_product. This is the standard Bitrix mechanism — no need to reinvent the wheel.
Order Routing
The router's job: when an order is created, parse the cart, group line items by supplier, and deliver each supplier their respective portion.
Complication — a single product may have multiple suppliers. Selection logic is required: by price, by availability, by priority. Implemented via UF_IS_PRIMARY plus a current stock check.
namespace Local\Dropshipping;
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Application;
class SupplierResolver
{
/**
* Returns the best supplier for a product:
* primary supplier with stock > 0 first, otherwise any supplier with stock
*/
public static function resolve(int $productId, int $quantity): ?array
{
$conn = Application::getConnection();
// Find suppliers with sufficient stock
$result = $conn->query("
SELECT sp.UF_SUPPLIER_ID, sp.UF_SUPPLIER_SKU, sp.UF_PURCHASE_PRICE,
sp.UF_IS_PRIMARY, csp.AMOUNT
FROM b_uts_supplier_product sp
JOIN b_catalog_store_product csp
ON csp.PRODUCT_ID = sp.UF_PRODUCT_ID
AND csp.STORE_ID = sp.UF_STORE_ID
WHERE sp.UF_PRODUCT_ID = {$productId}
AND csp.AMOUNT >= {$quantity}
ORDER BY sp.UF_IS_PRIMARY DESC, sp.UF_PURCHASE_PRICE ASC
LIMIT 1
");
return $result->fetch() ?: null;
}
}
Transmitting Orders to Suppliers
Three transmission channels, in order of preference:
1. Webhook (supplier REST API) — the preferred option. We POST JSON with order data to the supplier's URL; they respond with a confirmation:
private static function sendWebhook(string $url, string $apiKey, array $payload): bool
{
$http = new \Bitrix\Main\Web\HttpClient();
$http->setHeader('Content-Type', 'application/json');
$http->setHeader('Authorization', 'Bearer ' . $apiKey);
$http->setTimeout(10);
$response = $http->post($url, json_encode($payload));
$status = $http->getStatus();
\Bitrix\Main\Diag\Debug::writeToFile(
['url' => $url, 'status' => $status, 'response' => $response],
'Dropshipping webhook',
'/local/logs/dropshipping.log'
);
return $status === 200;
}
2. Email with an HTML table — when the supplier has no API. Email template via CEvent::Send, event DROPSHIPPING_ORDER_NEW. Includes a line-items table, delivery address, and amount payable.
3. File exchange via FTP/SFTP — for suppliers operating with 1C. We generate XML in CommerceML format and place it on the supplier's FTP server. They retrieve it on a schedule.
Stock Synchronisation
Stock levels go stale quickly — a fundamental problem of any dropshipping setup. Three strategies:
Push from the supplier — the supplier notifies us of stock changes via webhook. We implement an endpoint:
// /local/ajax/supplier/update-stock.php
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
$supplierId = SupplierAuth::validateKey($apiKey);
if (!$supplierId) {
http_response_code(403);
die(json_encode(['error' => 'Unauthorized']));
}
$items = json_decode(file_get_contents('php://input'), true)['items'] ?? [];
foreach ($items as $item) {
$productId = SupplierProduct::findBySupplierSku($supplierId, $item['sku']);
if ($productId) {
StockUpdater::updateSupplierStock($productId, $supplierId, (int)$item['quantity']);
}
}
echo json_encode(['updated' => count($items)]);
Scheduled pull — a Bitrix agent downloads the supplier's feed every 30 minutes and updates stock levels. Feeds come in XML (1C format), CSV, or JSON.
Reservation — if pull is not possible, immediately reduce stock in b_catalog_store_product for the supplier's warehouse upon order creation. Not accurate, but better than nothing.
Margin Calculation
The purchase price is stored in UF_PURCHASE_PRICE of the HL-block. The retail price is in b_catalog_price. The difference is the margin. A margin report is queried directly against the database:
SELECT
be.NAME AS product_name,
cp.PRICE AS retail_price,
sp.UF_PURCHASE_PRICE AS purchase_price,
cp.PRICE - sp.UF_PURCHASE_PRICE AS margin_abs,
ROUND((cp.PRICE - sp.UF_PURCHASE_PRICE)
/ cp.PRICE * 100, 1) AS margin_pct
FROM b_iblock_element be
JOIN b_catalog_price cp ON cp.PRODUCT_ID = be.ID AND cp.CATALOG_GROUP_ID = 1
JOIN b_uts_supplier_product sp ON sp.UF_PRODUCT_ID = be.ID AND sp.UF_IS_PRIMARY = 1
WHERE be.IBLOCK_ID = :catalog_iblock_id
AND be.ACTIVE = 'Y'
ORDER BY margin_pct ASC;
Handling Supplier Rejections
A supplier may reject an order (out of stock, address error). A status HL-block is needed for tracking:
b_uts_supplier_order
├── UF_ORDER_ID — Bitrix order ID (b_sale_order.ID)
├── UF_SUPPLIER_ID — supplier ID
├── UF_STATUS — pending / confirmed / rejected / shipped / delivered
├── UF_SUPPLIER_ORDER — order number at the supplier
├── UF_TRACKING — shipment tracking number
├── UF_REJECT_REASON — rejection reason
└── UF_DATE_UPDATE — last modified date
When status is rejected, an agent fires that notifies the manager and, if an alternative supplier exists for the same product, automatically re-routes the order.
Implementation Timeline
| Configuration | Scope | Timeline |
|---|---|---|
| Single supplier, email notifications | HL-blocks + handler + template | 1–2 weeks |
| Multiple suppliers, webhooks | + router + API sync | 3–4 weeks |
| Full setup with feeds, supplier portal, and analytics | + pull feeds + supplier account + margin report | 6–8 weeks |

