Integration with ATOL Online for 1C-Bitrix

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

ATOL Online Integration for 1C-Bitrix

Federal Law No. 54-FZ on online cash registers applies to most online stores: every non-cash payment from an individual must be accompanied by a fiscal receipt transmitted to the Fiscal Data Operator (FDO). ATOL Online is one of the largest cloud fiscalisation providers in Russia. Integration with 1C-Bitrix is technically non-trivial: you need to correctly form receipt line items, accurately specify VAT rates, payment subject and method attributes, handle the asynchronous callback from ATOL, and account for refund scenarios.

How ATOL Online Works

ATOL Online is not a physical cash register but a cloud service: your purchase data is sent to the ATOL API, ATOL passes it to a virtual cash register, which generates a fiscal document and sends it to the FDO (Fiscal Data Operator); the buyer receives an electronic receipt by email or phone.

Interaction scheme:

  1. Buyer pays for an order → payment is confirmed in your acquiring callback handler
  2. The store sends a request to the ATOL API (sell — income receipt)
  3. ATOL returns the task uuid (asynchronously)
  4. A few seconds later, ATOL sends a webhook with the fiscalisation result
  5. The store saves the fiscal document (FD number, FP) and marks the receipt as "processed"

Registering with ATOL and Obtaining Credentials

Before integration you will need:

  • Contract with ATOL and a registered virtual cash register
  • login and password — for obtaining the API token
  • group_code — cash register group code
  • inn — your organisation's Tax ID (INN) (for the receipt)
  • payment_address — settlement address (site URL)

Obtaining a Token

class AtolOnlineClient
{
    private const API_URL = 'https://online.atol.ru/possystem/v5/';
    private string $token = '';

    public function auth(string $login, string $password): void
    {
        $response = $this->request('getToken', [
            'login'    => $login,
            'pass'     => $password,
        ], false);

        if (empty($response['token'])) {
            throw new \RuntimeException('ATOL: authentication error — ' . ($response['error']['text'] ?? 'unknown'));
        }

        $this->token = $response['token'];
        // Cache the token for 24 hours (token lifetime — 24 hours)
        \Bitrix\Main\Data\Cache::createInstance()->set('atol_token', $this->token, 86000);
    }

    private function request(string $endpoint, array $data, bool $withToken = true): array
    {
        $headers = ['Content-Type: application/json; charset=utf-8'];
        if ($withToken) {
            $headers[] = 'Token: ' . $this->token;
        }

        $ch = curl_init(self::API_URL . $endpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($data, JSON_UNESCAPED_UNICODE),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        $result = json_decode(curl_exec($ch), true);
        curl_close($ch);
        return $result ?? [];
    }
}

Forming an Income Receipt

public function sell(string $groupCode, \Bitrix\Sale\Order $order, string $callbackUrl): array
{
    $basket  = $order->getBasket();
    $payment = $order->getPaymentCollection()->current();
    $buyer   = $order->getPropertyCollection();

    $items = [];
    foreach ($basket->getOrderableItems() as $item) {
        $items[] = [
            'name'           => mb_substr($item->getField('NAME'), 0, 128), // max 128 characters
            'price'          => round($item->getBasePrice(), 2),
            'quantity'       => $item->getQuantity(),
            'sum'            => round($item->getFinalPrice(), 2),
            'measurement_unit' => 'pcs',
            'payment_method' => 'full_payment',   // full payment
            'payment_object' => 'commodity',       // goods
            'vat'            => ['type' => 'none'], // no VAT; or 'vat10', 'vat20'
        ];
    }

    // Delivery as a separate line item (mandatory!)
    $deliveryPrice = $order->getDeliveryPrice();
    if ($deliveryPrice > 0) {
        $items[] = [
            'name'             => 'Delivery',
            'price'            => round($deliveryPrice, 2),
            'quantity'         => 1,
            'sum'              => round($deliveryPrice, 2),
            'measurement_unit' => 'service',
            'payment_method'   => 'full_payment',
            'payment_object'   => 'service',
            'vat'              => ['type' => 'none'],
        ];
    }

    $receiptPayments = [];
    // Determine the payment type for the receipt
    $paySystemCode = $payment->getPaymentSystemId();
    if (isCashPayment($paySystemCode)) {
        $receiptPayments[] = ['type' => 0, 'sum' => $order->getPrice()]; // cash
    } else {
        $receiptPayments[] = ['type' => 1, 'sum' => $order->getPrice()]; // non-cash
    }

    $payload = [
        'external_id' => 'BX_' . $order->getId() . '_' . time(),
        'receipt'     => [
            'client'   => [
                'email' => $buyer->getItemByOrderPropertyCode('EMAIL')?->getValue(),
                'phone' => $buyer->getItemByOrderPropertyCode('PHONE')?->getValue(),
            ],
            'company'  => [
                'email'           => COMPANY_EMAIL,
                'inn'             => COMPANY_INN,
                'payment_address' => SITE_URL,
                'sno'             => 'usn_income',  // tax system
            ],
            'items'    => $items,
            'payments' => $receiptPayments,
            'total'    => round($order->getPrice(), 2),
        ],
        'service'     => [
            'callback_url' => $callbackUrl,
        ],
        'timestamp'   => date('d.m.Y H:i:s'),
    ];

    return $this->request($groupCode . '/sell', $payload);
}

Common Errors When Forming a Receipt

Amount mismatch. receipt.total must exactly match the sum of payments[].sum. Fractional rounding is a frequent cause of the invalid_total_amount error.

Payment subject attribute. payment_object = commodity for goods, service for services (delivery, installation), payment for advances. An incorrect attribute leads to administrative liability.

Payment method attribute. payment_method = full_payment for full payment, prepayment for a prepayment, advance for an advance. In two-stage acquiring (hold + capture), the first receipt uses advance, the second uses full_payment.

Name length. ATOL limits name to 128 characters. Long names must be truncated.

Handling the ATOL Callback

ATOL asynchronously sends a POST to callback_url with the result:

// /bitrix/tools/atol_callback.php
$body = file_get_contents('php://input');
$data = json_decode($body, true);

// Verify the task UUID
$uuid     = $data['uuid'] ?? '';
$status   = $data['status'] ?? '';
$payload  = $data['payload'] ?? [];

// Find the order by uuid
$atolTask = AtolTaskTable::getList([
    'filter' => ['UUID' => $uuid],
])->fetch();

if (!$atolTask) {
    http_response_code(200);
    exit; // unknown task — ignore
}

if ($status === 'done' && !empty($payload['fiscal_document_number'])) {
    // Receipt successfully processed
    AtolTaskTable::update($atolTask['ID'], [
        'STATUS'                  => 'DONE',
        'FISCAL_DOCUMENT_NUMBER'  => $payload['fiscal_document_number'],
        'FISCAL_DOCUMENT_ATTRIBUTE' => $payload['fiscal_document_attribute'],
        'FNS_SITE'                => $payload['fns_site'] ?? '',
        'FN_NUMBER'               => $payload['fn_number'] ?? '',
        'SHIFT_NUMBER'            => $payload['shift_number'] ?? '',
        'RECEIPT_DATETIME'        => $payload['receipt_datetime'] ?? '',
    ]);
} elseif ($status === 'fail') {
    AtolTaskTable::update($atolTask['ID'], [
        'STATUS'    => 'FAIL',
        'ERROR'     => $data['error']['text'] ?? 'Unknown error',
        'ERROR_CODE'=> $data['error']['code'] ?? 0,
    ]);
    // Alert the administrator — receipt not processed
    notifyAdmin('ATOL Error', 'Order ' . $atolTask['ORDER_ID'] . ': ' . $data['error']['text']);
}

http_response_code(200);
echo 'OK';

Refund Receipt

When processing a refund to the buyer, a refund receipt (sell_refund) must be generated:

public function sellRefund(string $groupCode, \Bitrix\Sale\Order $order, float $refundAmount, array $refundItems): array
{
    $payload = [
        'external_id' => 'BX_REFUND_' . $order->getId() . '_' . time(),
        'receipt'     => [
            'client'   => ['email' => $buyerEmail],
            'company'  => ['inn' => COMPANY_INN, 'payment_address' => SITE_URL, 'sno' => 'usn_income'],
            'items'    => $refundItems,  // refund line items
            'payments' => [['type' => 1, 'sum' => $refundAmount]],
            'total'    => $refundAmount,
        ],
        'service'     => ['callback_url' => ATOL_CALLBACK_URL],
        'timestamp'   => date('d.m.Y H:i:s'),
    ];

    return $this->request($groupCode . '/sell_refund', $payload);
}

Integration with the Payment System Handler

The ATOL call is embedded in the payment confirmation event:

// In the OnSalePaymentEntitySaved event or in the acquiring callback handler
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale', 'OnSalePaymentEntitySaved',
    function(\Bitrix\Main\Event $event) {
        $payment = $event->getParameter('ENTITY');
        if ($payment->isPaid() && !$payment->isSystem()) {
            $order = $payment->getOrder();
            // Start fiscalisation
            $atol = new AtolOnlineService();
            $atol->fiscalize($order);
        }
    }
);

Timeline

Task Duration
Basic integration: income receipt + callback 2–3 days
Refund receipt + correct payment subject attributes 1–2 days
Monitoring: task table, error alerts 1 day
Testing on the ATOL test environment 1 day
Switch to production environment and acceptance 0.5 day