Developing a dropshipping scheme with 1C-Bitrix suppliers

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

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