Integrating 1C-Bitrix with Zebra label printers

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

Integrating 1C-Bitrix with Zebra Label Printers

Zebra is the industrial standard for thermal label printing. ZPL (Zebra Programming Language) is the command language that controls the printer. The integration goal with 1C-Bitrix is to automatically generate and send a label to the printer when a specific event occurs — goods receipt, order shipment, price change — without any operator involvement.

How Zebra Printing Works

Zebra printers communicate over the network via TCP/IP (port 9100 — RAW print) or through a web interface. ZPL commands are sent as plain text to a TCP socket:

function printZpl(string $printerIp, int $printerPort, string $zpl): bool
{
    $socket = fsockopen($printerIp, $printerPort, $errno, $errstr, 5);
    if (!$socket) {
        \Bitrix\Main\Diag\Debug::writeToFile("Zebra: $errstr ($errno)", '', '/bitrix/zebra_errors.log');
        return false;
    }
    fwrite($socket, $zpl);
    fclose($socket);
    return true;
}

For Windows environments with a shared printer — an alternative approach using a local Python/Node.js agent that receives jobs from 1C-Bitrix via HTTP and forwards them to the printer via WinSpool.

Generating a ZPL Product Label

Product label (price + barcode + SKU):

class ZebraLabelGenerator
{
    public function generatePriceLabel(array $product, string $priceType = 'BASE'): string
    {
        $price = \CCatalogProduct::GetOptimalPrice($product['ID'])['PRICE']['PRICE'] ?? 0;
        $barcode = $this->getBarcode($product['ID']);
        $name = mb_substr($product['NAME'], 0, 30); // truncated for 58mm label

        return implode("\n", [
            '^XA',                              // Start of label
            '^CI28',                            // UTF-8 encoding
            '^PW464',                           // Width 58mm (8 dots/mm)
            '^LL200',                           // Height 25mm
            // Product name
            '^FO20,10^A0N,24,24^FD' . $name . '^FS',
            // SKU
            '^FO20,40^A0N,18,18^FD' . ($product['PROPERTY_ARTICLE_VALUE'] ?? '') . '^FS',
            // Price
            '^FO20,65^A0N,36,36^FD' . number_format($price, 2, '.', ' ') . '^FS',
            // EAN-13 barcode
            '^FO20,110^BY2^BCN,50,Y,N,N^FD' . $barcode . '^FS',
            '^XZ',                              // End of label
        ]);
    }

    private function getBarcode(int $productId): string
    {
        $row = \Bitrix\Catalog\ProductBarcodeTable::getList([
            'filter' => ['PRODUCT_ID' => $productId],
            'limit'  => 1,
        ])->fetch();
        return $row['BARCODE'] ?? str_pad($productId, 12, '0', STR_PAD_LEFT);
    }
}

Label Templates in the Database

ZPL templates are stored in the bl_zebra_templates table rather than in code. This allows the layout to be changed without a deployment:

CREATE TABLE bl_zebra_templates (
    id          SERIAL PRIMARY KEY,
    code        VARCHAR(64) UNIQUE NOT NULL,  -- 'price_label', 'warehouse_label', 'shipment_label'
    name        VARCHAR(255),
    zpl_template TEXT NOT NULL,               -- ZPL with placeholders {{NAME}}, {{PRICE}}, {{BARCODE}}
    label_width  SMALLINT DEFAULT 58,         -- mm
    label_height SMALLINT DEFAULT 40,
    active       BOOLEAN DEFAULT true
);

The ZebraTemplateEngine class replaces placeholders with real data and sends the result to the printer.

Automatic Printing on Events

On price change (event OnProductUpdate or agent):

AddEventHandler('catalog', 'OnProductUpdate', function($productId) {
    $needPrint = \Bitrix\Main\Config\Option::get('zebra_module', 'auto_print_price_change', 'N');
    if ($needPrint !== 'Y') return;

    $product = \CIBlockElement::GetByID($productId)->GetNext();
    $printerIp = \Bitrix\Main\Config\Option::get('zebra_module', 'default_printer_ip');

    $zpl = (new ZebraLabelGenerator())->generatePriceLabel($product);
    ZebraPrinter::send($printerIp, 9100, $zpl);
});

On goods receipt — after processing the receipt document:

// In the receipt document handler
foreach ($receiptItems as $item) {
    for ($i = 0; $i < $item['qty']; $i++) {
        $jobs[] = [
            'product_id' => $item['product_id'],
            'copies'     => 1,
            'template'   => 'warehouse_label',
            'printer_id' => $item['warehouse_printer_id'],
        ];
    }
}
ZebraPrintQueue::push($jobs);

The print queue bl_zebra_print_queue is processed by an agent every minute. This is more reliable than direct printing inside an event handler — the printer may be temporarily unavailable.

Queue and Retries

CREATE TABLE bl_zebra_print_queue (
    id          SERIAL PRIMARY KEY,
    printer_id  INT NOT NULL,
    template_id INT NOT NULL,
    data_json   JSONB NOT NULL,
    status      VARCHAR(20) DEFAULT 'pending',
    attempts    SMALLINT DEFAULT 0,
    scheduled_at TIMESTAMP DEFAULT NOW(),
    printed_at  TIMESTAMP
);

On a print error, the status is set to retry and attempts is incremented. After 5 attempts the status becomes failed and the administrator is notified.

Timeline

Phase Duration
ZPL send class 1 day
Template engine with placeholders 2 days
Print queue + agent 1 day
Event handlers (price change, goods receipt) 2 days
Printer management admin interface 2 days
Testing with a real printer 1 day
Total 9–11 days