lamoda/atol-client
A PHP client for integrating ATOL online cash registers into your application. It handles fiscal receipts, correction documents, and cash register management, with Laravel support for quick setup.
lamoda/atol-client is a thin, typed PHP wrapper around ATOL Online's fiscalization REST API (v3/v4/v5). It gives you DTOs for receipts and a PSR-18-backed client for the five operations that matter: sell, sellRefund, buy, buyRefund, correction, plus getToken and getStatus.
Minimal setup:
composer require lamoda/atol-client
You bring your own HTTP stack — the client is PSR-18 / PSR-17 based, so any Guzzle/Symfony HttpClient + a request factory works:
use AtolClient\V5\Client;
use AtolClient\V5\Configuration;
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Psr7\HttpFactory;
$factory = new HttpFactory();
$http = new Guzzle(['timeout' => 15.0, 'connect_timeout' => 5.0]);
$client = new Client(
new Configuration(
login: env('ATOL_LOGIN'),
password: env('ATOL_PASSWORD'),
groupCode: env('ATOL_GROUP_CODE'),
baseUri: 'https://testonline.atol.ru/possystem/v5', // prod: online.atol.ru
),
$http,
$factory,
$factory,
);
Where to look first, in order:
README.md — the only end-to-end example of a receipt, and it usually targets the newest version.src/V5/Client.php — the public method surface; confirm the exact constructor signature after install, it has shifted between majors.src/V5/Dto/ — the object graph you actually have to populate (Receipt, Item, Company, Client, Payment, Agent, Supplier) and the enums (Vat, PaymentMethod, PaymentObject, MeasurementUnit, TaxationSystem).src/V5/Exception/ — the exception hierarchy, which maps 1:1 to ATOL error responses and is how you'll debug.First use case: one sell() call, then poll getStatus() in a loop until the state is terminal. Don't build refunds, corrections, or agent schemes until this works against the test endpoint.
$response = $client->sell($receipt, externalId: 'order-100500-1'); // returns uuid
$status = $client->getStatus($response->getUuid());
Wrap the client in a domain service. Never let AtolClient\V5\Client leak into controllers or jobs. Build a FiscalService with registerSale(Order $order): FiscalReceipt and a ReceiptFactory that maps your order/catalog model onto the ATOL DTO graph. This is the single highest-value abstraction: the DTO shape changes between v4 and v5, your domain model shouldn't.
Persist every attempt as a row. A fiscal_receipts table with external_id, uuid, operation, status, payload_hash, raw_response, attempt is non-negotiable for fiscal data. You need an audit trail for the tax authority and a state machine you can drive from a queue worker.
Idempotency via externalId. ATOL rejects a repeated externalId for the same operation, which is the only real duplicate protection you get. Use something deterministic and traceable (order-{id}-{attempt}) rather than a UUID — when a receipt shows up in the OFD and you need to reconcile it, a human-readable ID saves hours.
Treat submission and confirmation as two separate jobs. sell() only queues the document. A separate scheduled command should sweep rows in queued/wait state and call getStatus() with backoff (ATOL can take tens of seconds, occasionally minutes). Terminal states are done, fail, cancel; everything else needs another pass.
Token caching is your job. getToken() returns a token valid ~24h. In a multi-process worker fleet, cache it in Redis/memcached keyed by login + groupCode and refresh on 401; per-request re-auth will hammer ATOL and slow every receipt.
Adapter over the version folders. If you ship on v4 today and need v5 for FFD 1.2 tags, define your own FiscalClient interface with sell/refund/status, and implement it twice with a config switch. The DTOs are not interchangeable between versions.
Testing without the sandbox. The client accepts any PSR-18 implementation, so a fake that returns recorded JSON fixtures is faster and more deterministic than testonline.atol.ru. Assert on the serialized request body — most bugs are wrong VAT rate, wrong paymentObject, or a sum mismatch, all visible pre-flight.
Queue it. Every fiscal operation should be a queued job with tries and a dead-letter path. A shop checkout must not fail because ATOL timed out; it should complete and reconcile the fiscal receipt asynchronously.
sum must equal price * quantity and the sum of items must equal the payment total exactly. Compute in integer kopecks and convert at the boundary, otherwise error.code for sum mismatch will reject receipts unpredictably at scale.sell(). A timeout does not mean the document wasn't registered. Always getStatus() first — a duplicate retry is a second fiscal receipt, which is a real accounting problem, not just a 400.error block on failure. Check the DTO's error field / exception, not $response->getStatusCode().callbackUrl must be publicly reachable over HTTPS. If you're behind a VPC, either expose a narrow endpoint or skip callbacks entirely and rely on polling.Agent plus Supplier on both the receipt and each item. ATOL rejects with a validation error that often names only one missing tag — check the whole agent block at once.paymentObject and measurementUnit are enums with server-side code lists. A service (service) sold with a commodity code, or a piece good measured in PIECE when the catalog ships kg, produces a receipt the OFD will reject later. Map explicitly per SKU class, not with a default.Vat enum you'll have to patch the library or pin a newer release. Track upstream releases around regulatory effective dates.groupCode mismatch is the classic 401. Credentials can be valid while the group code is wrong; the error message doesn't always say so. Verify all three in the cabinet before debugging code.testonline.atol.ru does not enforce every FFD tag rule. Do at least one real low-value receipt against prod before launch.password and token), retry-with-backoff for getStatus only, and correlation IDs by decorating the HTTP client rather than forking the package.error.code: 12, the response alone tells you nothing; the body you sent plus the field-specific error.text does.How can I help you explore Laravel packages today?