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:
- Buyer pays for an order → payment is confirmed in your acquiring callback handler
- The store sends a request to the ATOL API (
sell— income receipt) - ATOL returns the task
uuid(asynchronously) - A few seconds later, ATOL sends a webhook with the fiscalisation result
- 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 |







