Cash Server API: reference
The protocol is compatible with ATOL Online: the same paths, the same field names, the same error codes. If you already have a module built for ATOL Online, changing the base URL and the access credentials is usually enough.
| Base URL (FFD 1.2) | https://54fz.cenaly.ru/possystem/v5/ |
| Base URL (FFD 1.05) | https://54fz.cenaly.ru/possystem/v4/ |
| Format | JSON, UTF-8, Content-Type: application/json |
| Authentication | a 24-hour token, the Token header |
| Liveness check | GET /possystem/health (no token needed) |
A product overview and the requirements for the cash register — Cash Server.
1. Authentication: getToken#
curl -X POST https://54fz.cenaly.ru/possystem/v5/getToken \
-H 'Content-Type: application/json' \
-d '{"login":"u9f3c1a7d40b2","pass":"••••••••"}'
{ "error": null, "token": "e826899dc378…063086", "timestamp": "04.08.2026 13:37:40" }
- The login and
group_codeare issued upon creating a POS server in the admin panel's "POS API" section and look likeu<12 hex>andkkt-<12 hex>— custom descriptive values cannot be chosen. - The token is valid for 24 hours; there is no need to request it for every receipt — cache it.
- The token is passed via the
Token: <token>header or the?token=<token>parameter. GET /possystem/v5/getToken?login=…&pass=…is also supported — mirroring ATOL.- Invalid credentials → HTTP 401, error code 12.
One login serves one POS group. We store the password only as an argon2id hash — it cannot be recovered, only reissued: "POS API" section → "Change password". Previously issued tokens will continue to work until their 24 hours expire.
2. Registering a document#
POST /possystem/{v5|v4}/{group_code}/{operation}
| Operation | What document it is | v5 | v4 |
|---|---|---|---|
sell |
Sale (income) | ✅ | ✅ |
sell_refund |
Refund of a sale | ✅ | ✅ |
buy |
Expense | ✅ | ✅ |
buy_refund |
Refund of an expense | ✅ | ✅ |
sell_correction |
Correction of a sale | ✅ | ✅ |
buy_correction |
Correction of an expense | ✅ | ✅ |
sell_refund_correction |
Correction of a sale refund | ✅ | — |
buy_refund_correction |
Correction of an expense refund | ✅ | — |
Request body#
{
"timestamp": "04.08.2026 14:40:00",
"external_id": "order-10421",
"service": { "callback_url": "https://shop.example/api/fiscal-callback" },
"receipt": {
"client": { "email": "buyer@example.com" },
"company": {
"email": "shop@example.com",
"sno": "usn_income",
"inn": "7736570901",
"payment_address": "https://shop.example"
},
"items": [
{
"name": "Coffee beans, 1 kg",
"price": 1250.00,
"quantity": 2,
"sum": 2500.00,
"measure": 0,
"payment_method": "full_payment",
"payment_object": 1,
"vat": { "type": "vat20" }
}
],
"payments": [ { "type": 1, "sum": 2500.00 } ],
"total": 2500.00
}
}
| Field | Required | Description |
|---|---|---|
timestamp |
yes | The moment of settlement, format dd.mm.yyyy HH:MM:SS |
external_id |
yes | Your document identifier, ≤ 128 characters. The idempotency key (see §5) |
service.callback_url |
no | The HTTP(S) address for delivering the result, ≤ 256 characters |
receipt |
yes | The receipt body for ordinary operations |
correction + correction_info |
yes | The body and the grounds for *_correction operations |
The contents of receipt and all value reference tables — Receipt fields.
Response#
{ "uuid": "47b30214-b145-4c3f-abe3-89076225ac4f",
"timestamp": "04.08.2026 13:39:15", "status": "wait", "error": null }
uuid is the identifier of the document in our queue; the result is requested by it. The wait status means "accepted, waiting for a cash register" — that is the normal response to a successful registration.
Important. A
waitresponse does not yet mean the receipt has been registered. A payment may be treated as fiscalized only ondonein the report or in the callback.
3. Result: report#
GET /possystem/{v5|v4}/{group_code}/report/{uuid}
curl "https://54fz.cenaly.ru/possystem/v5/kkt-1f4c9a20b73e/report/47b30214-…" \
-H "Token: $TOKEN"
{
"uuid": "47b30214-b145-4c3f-abe3-89076225ac4f",
"timestamp": "04.08.2026 13:39:28",
"callback_url": "https://shop.example/api/fiscal-callback",
"status": "done",
"group_code": "kkt-1f4c9a20b73e",
"daemon_code": "cenaly-54fz",
"device_code": "kkt-1",
"external_id": "order-10421",
"error": null,
"warnings": null,
"payload": {
"fiscal_receipt_number": 101,
"shift_number": 9,
"receipt_datetime": "04.08.2026 13:39:15",
"total": 2500.0,
"fn_number": "9999078902005478",
"ecr_registration_number": "0000000001056264",
"fiscal_document_number": 6789,
"fiscal_document_attribute": 2745081395,
"fns_site": "www.nalog.gov.ru",
"ofd_receipt_url": ""
}
}
Statuses#
status |
Meaning | What to do |
|---|---|---|
wait |
Queued, POS has not responded yet | Wait for callback or poll |
done |
Document issued, payload populated |
Save details in your order |
fail |
Document not issued, error populated |
Parse the error code, see Errors |
Payload fields#
| Field | FFD tag | Description |
|---|---|---|
fiscal_receipt_number |
1042 | Receipt number per shift |
shift_number |
1038 | Shift number |
receipt_datetime |
1012 | Document time according to POS clock |
total |
1020 | Receipt total in rubles |
fn_number |
1041 | Factory serial number of the fiscal storage (FN) |
ecr_registration_number |
1037 | Registration number of the POS/ECR (RN KKT) |
fiscal_document_number |
1040 | Fiscal document number (FD) |
fiscal_document_attribute |
1077 | Fiscal document sign/attribute (FPD) |
fns_site |
1060 | FTS website for receipt verification |
ofd_receipt_url |
— | Link to the receipt in OFD; currently always empty (see below) |
The four parameters FD + FPD + FN + RN KKT are what you need to save in the order: they allow locating the receipt in the OFD personal dashboard and in the FTS "Receipt Check" mobile app. The ofd_receipt_url field is reserved by the protocol, but POS drivers do not return the link, so it arrives empty.
Certain POS units may not return some attributes (such as the shift number). In this case, the field returns zero or empty — we deliberately never substitute calculated values into fiscal attributes.
Storage and limits#
- The result is available for at least 32 days; the document is archived after 60 days.
- Polling is rate-limited: 1 request per second per POS in the group. Exceeding it returns HTTP 429.
- Unknown
uuid→ error 30; document still in queue → HTTP 200 withstatus: wait.
4. Callback#
If the request contained service.callback_url, then after the document moves to done or fail we send it a POST with the same body the report returns.
- The request timeout is 10 seconds, success is any 2xx response.
- Retries: immediately, after 30 seconds, after 120 seconds (3 attempts in total).
- An invalid or overly long URL does not break the receipt: it is discarded and
warnings.callback_urlappears in the report.
The handler on your side must be idempotent — key it on uuid or external_id. The callback does not replace polling: if no notification arrived within 300 seconds, request the report yourself.
5. Idempotency#
external_id is unique within a group. Registering the same external_id again returns HTTP 400 and error 33 — together with the original uuid:
{ "uuid": "47b30214-…", "status": "wait",
"error": { "code": 33, "text": "a document with this external_id has already been accepted in this group", "type": "system" } }
That is the regular answer to a retry, not an emergency: take the uuid from the response and request the report for it.
Idempotency is end-to-end — it reaches the cash register itself. The bridge receives an operation key built from the group and external_id, and the bridge keeps an operation journal on disk: a repeat of the same operation returns the first result and survives a reboot of the cash register PC. If the previous attempt broke off with an unknown outcome (the connection dropped exactly at the closing of the receipt), the repeat is rejected with a demand to verify the last document on the cash register — a second tax document is never generated blindly.
The practical rule: one external_id per payment, and repeat the request with the same value as many times as you like. Create a new external_id only for a new settlement.
6. Limits#
| Limit | Value |
|---|---|
| Document registration | 10 requests per second per group (soft; above that — HTTP 429) |
| Report polling | 1 rps × the number of cash registers in the group |
| Request body size | 256 KB |
| Items in a receipt | 100 |
Length of an item's name |
128 characters |
Length of external_id |
128 characters |
Length of callback_url |
256 characters |
| Token lifetime | 24 hours |
| Waiting for a free cash register | 300 seconds, then fail / timeout |
7. Error Codes#
The error object format is consistent across all endpoints:
{ "error": { "error_id": "f546e8d0-…", "code": 33, "text": "…", "type": "system" },
"status": "fail", "timestamp": "04.08.2026 13:39:15" }
type — one of system, driver, agent, timeout, unknown.
| Code | HTTP | Meaning |
|---|---|---|
| 0 / 1 | 400 | Unknown error / processing failure |
| 10 | 401 | Token not provided |
| 11 | 401 | Token expired or unknown |
| 12 | 401 | Invalid login or password |
| 13 | 400 | Request validation error |
| 20 | 401 | Token does not match group |
| 21 | 401 | Group does not support this protocol version |
| 30 | 400 | Unknown uuid |
| 31 | 400 | Unsupported operation |
| 32 | 400 | Receipt JSON validation error (text specifies the field) |
| 33 | 400 | external_id already accepted (response contains previous uuid) |
| 34 | 200 | Document has not been processed yet (status: wait) |
| 40 | 400 | Bad request |
| 41 | 415 | Unsupported Content-Type |
| 50 | 500 | Service configuration error |
Codes 2001–2003 — no available POS / no matching POS / TIN mismatch:
- no POS became available within 300 seconds (every POS in the group is offline or busy with other receipts) →
status: fail,type: "timeout", code 2001. If a POS did take the receipt but did not answer within 300 s, you get code 1 with the sametype: "timeout"— the code tells "no POS at all" from "the POS went silent"; - receipt TIN does not match group TIN → document is rejected immediately upon registration: HTTP 400, code 2003, text indicates both TINs; it does not enter the queue;
- POS cannot process this receipt type (marking, electronic receipt, extended buyer requisites) →
status: fail, code 2002,type: "driver", with the POS's verbatim rejection inerror.text.
Driver errors do not arrive in the registration response, but in the document result — they occur once it has already been accepted into the queue. For a detailed breakdown and resolution steps for each case, see Errors and Diagnostics.
8. Differences between v5 and v4#
| v5 (FFD 1.2) | v4 (FFD 1.05) | |
|---|---|---|
| Unit of measurement | measure — numeric code (tag 2108), required |
measurement_unit — string ("pcs", "kg") |
| Subject of calculation | payment_object — number 1…33, required |
payment_object — string |
| Payment method | payment_method is required |
Optional; if not passed, tag 1214 is not sent to the POS and it applies its default flag |
| Marking code | mark_code — object with a single format field |
nomenclature_code — hex string |
| Return corrections | Yes | No |
They cannot be mixed: measurement_unit in v5 and measure in v4 are rejected by validation with a clear message. v5 is recommended for new integrations — marked goods require FFD 1.2.
9. Quick Start: Full Cycle with curl#
BASE=https://54fz.cenaly.ru/possystem/v5
GROUP=kkt-1f4c9a20b73e
TOKEN=$(curl -s -X POST $BASE/getToken -H 'Content-Type: application/json' \
-d '{"login":"u9f3c1a7d40b2","pass":"••••"}' | jq -r .token)
UUID=$(curl -s -X POST $BASE/$GROUP/sell -H "Token: $TOKEN" \
-H 'Content-Type: application/json' -d @receipt.json | jq -r .uuid)
curl -s "$BASE/$GROUP/report/$UUID" -H "Token: $TOKEN" | jq .payload
Next: receipt details · store scenarios · errors.