Fiscal Core API012 — справочник интеграции

Источник

Эта спека построена на официальной документации протокола API012 V1 (cashbox.ru / АРМАКС, 94 страницы, единственный авторитетный источник по контракту). Reverse-engineering APK использовался только для перекрёстной проверки (порты, broadcast’ы, hardware абстракция). Если PDF и APK расходятся — верь PDF.

Финальная проверка — только на K10 + Windows

Apple Silicon Mac не может запускать armeabi-v7a APK даже в эмуляторе (Google убрал 32-bit ARM bridge из всех arm64 system images). Для эмпирической проверки нужен либо физический K10, либо Windows/x86_64 Linux машина с ARM-эмулятором.

Архитектура процессов на устройстве

┌──────────────────────────────────────────────────────────┐
│  Unitodi K10-F (Android 8+, Centerm SmartPOS HAL)        │
│                                                           │
│  ┌────────────────┐         ┌──────────────────────┐     │
│  │  erp-pos       │         │  pbfk10 (ФЯ)         │     │
│  │  (React Native)│         │  ru.armax.cashbox.   │     │
│  │                │         │   pbfk10              │     │
│  │  FiscalCore    ├─HTTP───►│  CashboxHttpService  │     │
│  │  Client        │ :8088   │  :cashboxhttpservice │     │
│  │                │ /api012 │   proc                │     │
│  │                │ /v1/    │                      │     │
│  │                │         │  CashboxService      │     │
│  │                │         │  :cashboxservice     │     │
│  │                ◄─Bcast───│   proc                │     │
│  │                │ status_ │  ↓                    │     │
│  │                │ internal│  FsApi               │     │
│  └────────────────┘         │  (Serial → ФН)       │     │
│                              └──────────┬───────────┘     │
│                                         ▼                 │
│                                  ┌──────────────┐         │
│                                  │ ФН           │         │
│                                  │ (физический  │         │
│                                  │  модуль ФНС) │         │
│                                  └──────┬───────┘         │
└─────────────────────────────────────────┼─────────────────┘
                                          ▼
                                    ┌─────────┐
                                    │  ОФД    │
                                    └─────────┘
  • ФЯ = Qt5 C++ приложение в двух процессах: :cashboxserviceproc (фискальная логика, обмен с ФН и ОФД) и :cashboxhttpserviceproc (HTTP сервер для внешних клиентов)
  • Java-обёртка минимальная: shim’ы Activity/Service + FsApi (прямой Serial-IO в ФН через com.pos.sdk.exserialport.ExSerialPortDevice) + ServiceStarter (autostart на BOOT_COMPLETED)
  • Hardware HAL — Centerm SmartPOS SDK (com.centerm.smartpos.aidl.*, com.pos.sdk.*), общий для всего семейства SmartPOS-устройств
  • Касса не лезет в ФН напрямую — только через HTTP API ФЯ

Транспорт

ПараметрЗначение
ПротоколHTTP/1.1
Порт по умолчанию8088
Базовый путь/api012/v1/
Content-Typeapplication/json (с UTF-8)
HTTP методыGET, POST
АвторизацияHTTP Basic, login/password = креды кассира из локальной базы ФЯ
ИдемпотентностьПараметр externalId — повторный запрос с тем же ID вернёт результат предыдущего

Авторизация — это креды кассира ФЯ, не сервисный токен

У ФЯ есть своя локальная база кассиров с правами (см. loadcashiers.json). Каждый запрос требует Basic Auth с логином/паролем кассира из этой базы. Ролей нет — есть гранулярные permissions (Receipts, CycleClose, Reports, Settings…).

Для нашей интеграции: заранее создаём в ФЯ одного «сервисного» кассира с полными правами через POST savecashier.json, креды кладём в env-переменные erp-pos. Реальное ФИО кассира из нашего PIN-логина передаём в каждом запросе как поле cashier в document.

Контракт ответа

Любой ответ — JSON-объект с реквизитом result:

resultЗначение
0Успех
1..1535Бизнес-ошибка ФЯ (см. таблицу ошибок)
64Документ с таким externalId уже существует — это нормальный случай идемпотентности, надо забрать готовый документ
33554432..33555432HTTP-статус не 200 — result & 0xFFF = HTTP-статус, message транслитом

HTTP 200 + result=0 ⇒ операция выполнена. Если ответ не содержит result поля — значит ответил не ФЯ (кто-то другой захватил порт).

Общие query-параметры

ПараметрТипНазначение
externalidstringИдемпотентность (≤255 байт). Если не передать — ФЯ сгенерирует
clientIdstringИдентификатор клиентского приложения, эхо в ответе
silent0|11 — не печатать чек на принтере (для электронных чеков)
brief0|11 — краткая печатная форма отчёта
short0|11 — короткий формат документа в архиве
fdintНомер фискального документа

Регистр имени параметра

В документации в одном месте externalid, в другом externalId. На реальном устройстве проверить, какое имя действительно работает (или оба). Для входных JSON в body — externalId (camelCase).

Эндпоинты

Авторизация и кассиры

МетодПутьНазначение
GET/api012/v1/login.jsonТекущий кассир + permissions
GET/api012/v1/loadcashiers.jsonСписок кассиров (≤200, пагинация по cashierId)
GET/api012/v1/removecashier.json?cashierId=NУдалить кассира
POST/api012/v1/savecashier.jsonСоздать/обновить кассира

Permissions (флаги в cashier.permissions): Corrections, CycleClose, CycleOpen, Fiscalization, OfflineNotifications, Receipts, Reports, ReturnCorrections, ReturnReceipts, Settings, WriteSerial.

Состояние ККТ

МетодПутьНазначение
GET/api012/v1/cashboxstatus.jsonПолный статус (dev, fs, model, reg) — основной мониторинг
GET/api012/v1/cashboxstatus.htmlТо же, HTML для отладки
GET/api012/v1/cashboxserial.jsonСерийник + модель
GET/api012/v1/fscounters.jsonСчётчики ФН за всё время
GET/api012/v1/cyclecounters.jsonСчётчики текущей смены

Структура cashboxstatus.json (ключевое для UI кассы)

{
  "result": 0,
  "status": {
    "dev": {
      "accumLevel": "55.0%",       // заряд аккумулятора
      "ipAddresses": "192.168.0.13",
      "versionText": "0.0.30",
      "printerStatus": 0           // 0 = ОК, иначе ошибка
    },
    "dt": "2022-03-01T12:01:53+03:00",
    "fs": {
      "availableDocs": 249785,     // сколько ещё документов влезет в ФН
      "currentFfd": "1.2",
      "cycle": 7,                  // номер текущей смены
      "cycleCloseDt": null,
      "cycleIsOpen": true,         // ◄─ ключевое для UX
      "cycleOpenDt": "2021-11-29T11:00:00+03:00",
      "freeBufSize": 14068,        // КБ свободно для документов 30 хранения
      "labelBufUsed": 1,           // % заполнения буфера маркировок
      "lifeTime": {
        "expiredDays": 465,        // дней до окончания срока действия ФН
        "expiredDt": "2023-03-09",
        "regsCount": 2,
        "availableRegs": 28
      },
      "notifications": {
        "firstNotification": 0,
        "memFullPercent": 1,
        "notSendedCount": 0,       // ◄─ не отправлено в ОИСМ
        "status": 0
      },
      "receipt": 0,                // чеков в смене
      "release": "ФН-1.1М МГМ ...",
      "status": {
        "lastFd": 26,              // номер последнего ФД
        "phase": 3,                // фаза жизни ФН
        "serial": "9999078902009303"
      },
      "transport": {
        "firstDocDt": null,
        "firstDocNumber": 0,
        "messagesCount": 0,        // ◄─ не отправлено в ОФД
        "ofdMessageReading": false,
        "status": 0
      }
    },
    "hasNotPrinted": false,        // ◄─ есть недопечатанный документ — позвать printnotprinted.json
    "model": {
      "model": 211,
      "modelName": "ЛИМОН БАНК-Ф",
      "serial": "11200401",
      "version": "012"
    },
    "reg": { /* регистрационные данные ККТ */ }
  }
}

Что слушать в касса: cycleIsOpen, printerStatus, hasNotPrinted, notSendedCount, messagesCount, expiredDays, availableDocs. Из этих полей собирается «здоровье ККТ» в шапке экрана кассира.

Управление сменой

МетодПутьНазначение
GET/api012/v1/cycleopen.jsonОткрыть смену (минимальный вариант, без доп.данных)
POST/api012/v1/cycleopen.jsonОткрыть смену (с cashier, additionalParam, header, footer)
GET/api012/v1/cycleclose.jsonЗакрыть смену (Z-отчёт)
POST/api012/v1/cycleclose.jsonЗакрыть смену (с doBankSettlement)
GET/api012/v1/xreport.jsonX-отчёт (без обнуления, нефискальный)
GET/api012/v1/fsreport.jsonОтчёт ФН (нефискальный)

POST cycleopen.json — body

{
  "document": {
    "cashier": "Иванова Степанида Карловна",
    "additionalParam": "CYCLE_PARAM",
    "additionalData": "21FFCE01",
    "header": "Текст заголовка",
    "footer": "Текст подвала"
  }
}

POST cycleclose.json — body

{
  "document": {
    "cashier": "Иванова Степанида Карловна",
    "additionalParam": "CYCLE_PARAM",
    "additionalData": "21FFCE01",
    "header": "Текст заголовка",
    "footer": "Текст подвала",
    "doBankSettlement": true,                       // банковская сверка при закрытии
    "bankSettlementErrorIsCycleClosingError": true  // блокировать Z-отчёт если сверка не прошла
  }
}

Ответ Z-отчёта содержит полные cycleCounters с разбивкой по debit/debitRefund/credit/creditRefund × vat0/10/10_110/20/20_120/none × cash/card/prepay/postpay/barter. Эти счётчики надо отправлять в наш бэкенд для отчётности.

Чеки

МетодПутьНазначение
POST/api012/v1/receipt.jsonРегистрация фискального чека (приход/расход/возвраты)
POST/api012/v1/correction.jsonЧек коррекции (то же тело + обязательное correctionReason)
GET/api012/v1/introduction.json?cash=NВнесение наличных (нефискальное) — N в копейках
GET/api012/v1/payout.json?cash=NВыплата/изъятие наличных — N в копейках

POST receipt.json — body

{
  "document": {
    "cashier": "СИС. АДМИН",
    "cashierInn": "",
    "address": "Тульская область, деревня Селезневка, Аэропорт",
    "place": "Окно 13",
    "tax": 1,                          // система налогообложения (см. таблицу)
    "paymentAttr": 1,                  // 1=приход, 2=возврат прихода, 3=расход, 4=возврат расхода
    "buyerPhone": "client@example.com",
    "internetPayment": false,
    "header": "Текст заголовка",
    "footer": "Текст подвала",
    "ctext": "Текст внутри чека, перед позициями",
 
    "operations": [                    // позиции БЕЗ маркировки
      {
        "name": "Кофе",
        "type": 1,                     // признак предмета расчёта (см. таблицу)
        "paymentType": 4,              // признак способа расчёта (см. таблицу)
        "price": "455.00",             // цена за единицу — СТРОКА В РУБЛЯХ
        "quantity": "1.000",           // количество — строка с дробью
        "vatRate": 1,                  // ставка НДС (см. таблицу)
        "productCode": {               // штрихкод (опционально)
          "type": 1302,                // EAN-13
          "data": "4607001770459"
        },
        "agentFlag": 64,               // если позиция от агента (опционально)
        "providerInn": "7725225244",
        "providerData": { "name": "ООО ТУГРИК", "phones": ["+723685555"] },
        "countryCode": "Uz",
        "declarationNumber": "Gyjknnj"
      }
    ],
    "labledOperations": [              // позиции С маркировкой (отдельный список!)
      // та же структура + дополнительно
      // checkLabelFlags, labelCheckResult (из labelcheck.json),
      // industryProperties, fraction
    ],
 
    "cash": "455.00",                  // суммы оплаты по типам (строки в рублях)
    "card": "0.00",
    "prepay": "0.00",
    "postpay": "0.00",
    "barter": "0.00",
 
    "cardPayments": [                  // тег ФФД 1234 — детали безналичных оплат
      {
        "amount": "455.00",            // сумма (строка в рублях)
        "wayType": 1,                  // 0..255 способ безналичной оплаты
        "ids": "RRN-25434534534",      // идентификаторы (≤256 байт)
        "additional": "auth=12345"     // доп.данные (≤256 байт)
      }
    ],
 
    "iBank": {                         // ОПЦИОНАЛЬНО — встроенный эквайринг (см. ниже)
      "usePosFeature": true,
      "rrn": "25434534534",
      "transactionId": "25434534534",
      "authCode": "25434534534",
      "merchantNumber": 1,
      "bankClientApp": "ru.example.pos"
    }
  }
}

Ответ receipt.json

{
  "result": 0,
  "document": {
    /* всё что было в запросе + */
    "docNumber": 39,                   // тег 1040 — номер ФД
    "fsNumber": "9999078902009303",    // тег 1041 — номер ФН
    "fiscalCode": "1335618438",        // тег 1077 — ФПД
    "qr": "<строка для QR>",           // тег 1196
    "dt": "2021-11-30T10:09:26+03:00",
    "cycle": 11,                       // номер смены
    "receipt": 1,                      // номер чека за смену
    "externalId": "La/IGZVl3tFr0r0qV8hmUdylwAA=",
    "ftsSite": "nalog.ru",
    "vat20": "75.83"                   // авторасчёт НДС от ФЯ
  }
}

Все суммы — строки рублей с двумя десятичными

Цены, итоги, НДС, оплаты — всё передаётся как строки вида "455.00". НЕ копейки, НЕ числа. Это касается price, quantity, cost, cash, card, prepay, postpay, barter, vat20, vat10, amount в cardPayments и т.д.

Исключение: introduction.json и payout.json берут cash в query-параметре в копейках (?cash=100000 = 1000 ₽). Это легаси GET-эндпоинты старого стиля.

Справочные таблицы

paymentAttr (тег 1054, признак расчёта):

КодЗначение
1Приход (продажа)
2Возврат прихода
3Расход
4Возврат расхода

tax (тег 1055, СНО):

КодЗначение
1ОСН
2УСН доход
4УСН доход минус расход
8ЕНВД
16ЕСХН
32ПСН

vatRate (тег 1199, ставка НДС):

КодСтавка
120%
210%
320/120 (расчётная)
410/110 (расчётная)
50%
6НДС не облагается
75%
87%
95/105
107/107
1122%
1222/122

paymentType (тег 1214, признак способа расчёта):

КодЗначение
1Полная предоплата 100%
2Частичная предоплата
3Аванс
4Полный расчёт (для общепита — это то что нам нужно)
5Частичная оплата + кредит
6Передача в кредит
7Оплата кредита

type (тег 1212, признак предмета расчёта): Для общепита нас интересуют:

КодЗначение
1Товар
2Подакцизный товар
3Работа
4Услуга
13Иной предмет расчёта
30..33Маркированные/немаркированные товары и подакцизные с КМ

Полная таблица 27 значений (включая лотереи, ставки, агентские) — в PDF стр. 70-73.

productCode.type (типы кодов товара):

КодТип
1300Неизвестный
1301EAN-8
1302EAN-13
1303ITF-14
1304GS1.0
1305GS1.M
1306КМК
1307МИ
1308ЕГАИС-2.0
1309ЕГАИС-3.0
1320..1325Ф.1..Ф.6

unit (тег 2108, мера количества):

КодЕд.
0шт./ед.
10..12г / кг / т
20..22см / дм / м
30..32см² / дм² / м²
40..42мл / л / м³
50..51кВт·ч / Гкал
70..73сутки / час / мин / с
80..83КБ / МБ / ГБ / ТБ
255иное

POST correction.json

То же тело что у receipt.json + обязательное correctionReason:

{
  "document": {
    /* ... все поля как у receipt ... */
    "correctionReason": {
      "isIndependent": true,           // true=независимая, false=по предписанию
      "date": "2021-11-30",            // дата корректируемого расчёта
      "docNumber": "12345"             // номер предписания налогового органа (если есть)
    }
  }
}

Маркировка (Честный знак)

Последовательность для каждого чека с маркой:

  1. GET /api012/v1/startlabelschecksession.json — очистить таблицу проверок марок в ФН (макс 128 марок на чек!)
  2. Для каждого маркированного товара: POST /api012/v1/labelcheck.json с rawLabel и excpectedStatus
  3. Результаты подложить в labelCheckResult соответствующих позиций в labledOperations
  4. POST /api012/v1/receipt.json или POST /api012/v1/correction.json

Полный контракт labelcheck.json см. в PDF стр. 83-86. Для MVP общепита — Phase 2+.

Архив ФН

МетодПутьНазначение
GET/api012/v1/ofdticket.json?fd=NКвитанция ОФД по номеру ФД
GET/api012/v1/findfsdoc.json?fd=NНайти документ в ФН
GET/api012/v1/tlvdoc.json?fd=NTLV-представление документа (доступно 30 дней)
GET/api012/v1/regdoc.json?fd=N&short=0|1Документ о регистрации/перерегистрации

Печать и изображения

МетодПутьНазначение
POST/api012/v1/print/<name>.bbПечать произвольного BBCode-текста (body = текст в UTF-8)
POST/api012/v1/images/<fileName>Загрузить изображение в ККТ
GET/api012/v1/images/<fileName>Скачать изображение
GET/api012/v1/images/list.jsonСписок загруженных изображений
GET/api012/v1/printnotprinted.jsonДопечатать недопечатанный документ (hasNotPrinted=true в статусе)

Регистрация ККТ (разово)

МетодПутьНазначение
POST/api012/v1/preprintregistration.jsonПредпечать отчёта о регистрации
POST/api012/v1/registrate.jsonРегистрация в ФНС
POST/api012/v1/calcreport.jsonРасчётный отчёт
GET/api012/v1/fsclose.jsonЗакрытие архива ФН

Это бухгалтерские операции. Наша касса их не делает — выполняются разово через UI ФЯ при пусконаладке.

Встроенный эквайринг (iBank)

Опционально и не для всех ККТ

Документация явно квалифицирует: «Объект только для андроидных касс с банковским терминалом». То есть это класс оборудования (например, ЛИМОН БАНК-Ф из примеров PDF, где касса+эквайринг в одном корпусе), а не общая фича. Применимо ли к Unitodi K10-F — надо проверить эмпирически на физическом устройстве. В strings APK строки iBank, usePosFeature, bankClientApp не найдены, что косвенно указывает на отсутствие реализации в этом конкретном билде.

В чек продажи (receipt.json) можно добавить опциональный объект iBank. Когда он передан и card > 0, ФЯ самостоятельно:

  1. Запускает встроенное банковское приложение для оплаты картой
  2. Ждёт результата авторизации
  3. Если успех — фискализирует чек
  4. Запускает приложение из bankClientApp (например, нашу кассу) для возврата фокуса
"iBank": {
  "usePosFeature": true,        // true → запустить оплату картой на встроенном POS-терминале
  "rrn": "25434534534",         // только для возвратов — RRN исходной операции
  "transactionId": "25434534534",  // только для возвратов
  "authCode": "25434534534",       // только для возвратов
  "merchantNumber": 1,             // если у POS несколько мерчантов
  "bankClientApp": "ru.example.pos"  // куда вернуться после операции
}

Параллельный механизм: при закрытии смены cycleclose.json можно передать doBankSettlement: true — ФЯ автоматически выполнит сверку итогов с банком.

Подробное сравнение iBank-режима с внешней оркестрацией эквайринга через нашу кассу — в [[08-Specs/POS/Sale Flow|Sale Flow]].

Коды ошибок

Полный перечень в PDF стр. 91-94. Ключевые для нашей логики:

Авторизация и доступ

КодОписаниеДействие кассы
0Без ошибок
4Некорректные данные кассираПерепроверить креды в env
14Доступ запрещёнКассиру нет нужного permission

Состояние кассы

КодОписаниеДействие кассы
23ККТ не фискализированаНужна разовая регистрация через UI ФЯ
24Смена уже открытаOK для cycleopen (повтор) — игнор
25Смена закрытаОткрыть смену перед чеком
26Смена превысила 24 часаЗаблокировать UI кассы, пока не закроют смену
32В ФН есть неотправленные документыБаннер «не отправлено в ОФД: N», операции продолжать

Чек / суммы

КодОписаниеДействие кассы
28Сумма картой превышает сумму чекаПроверить расчёт оплаты
29Недостаточно средств для оплаты чекаДоплатить
33Не хватает реквизитовЛогировать, дёрнуть тех.поддержку
45В чеке нет предметов расчётаНе должно случаться при валидации
46Чек не оплаченcash + card + … < amount
47Переплата по чекуcash + card + … > amount
48Некорректная налоговая системаПроверить tax
49Некорректная сумма по чекуПроверка

Идемпотентность

КодОписаниеДействие кассы
64Документ с таким externalId уже существуетЭто нормально. Запросить готовый документ через findfsdoc.json

Принтер

КодОписаниеДействие кассы
65Есть недопечатанный документВызвать printnotprinted.json
975, 1000-1011Ошибки принтераКассиру: «нет бумаги / перегрев / ошибка», деталь по коду
1005Нет бумагиПрямой алерт «вставьте ленту»

ФН

КодОписаниеДействие кассы
1281-1342Ошибки ФН (раскодированные команды ФН → human-readable текст в message)Логировать, эскалировать
1297Нет транспортного соединенияФН не отвечает
1298Исчерпан ресурс ФНЗамена ФН — критично
1302Превышена продолжительность сменыАналогично коду 26
1533-1535Ошибки разбора/связи с ФНТех.поддержка

HTTP-обёртка

КодОписание
33554432-33555432HTTP-статус не 200, result & 0xFFF = HTTP, message транслитом

Open questions для проверки на K10

  • Порт по умолчанию — действительно 8088 на K10? (PDF говорит 8088, но в UI ФЯ может быть переопределён)
  • Логин/пароль кассира по умолчанию — спросить у поставщика или прочитать из login.json через UI ФЯ
  • iBank.usePosFeature работает ли в этом билде? Послать receipt.json с card>0 и iBank.usePosFeature=true → реально ли запустится банковское приложение? (Нужно предварительно установить какое-то банковское приложение, например INPAS Smart Sale или сберовский Pay)
  • Список bankClientApp — какие банковские APK поддерживаются интеграцией iBank? Не указано в PDF
  • /print/<name>.bb BBCode синтаксис — приложение к PDF, в выгруженном тексте отсутствует. Скачать оригинал или экспериментировать
  • Регистр имени параметра externalid vs externalId в query — оба или один?
  • Поведение при cycle > 24h — точно ли коды 26 и 1302 эквивалентны
  • offlineNotifications.bin/.zip — формат файла для ОИСМ в автономном режиме (нам не нужно для MVP, но зафиксировать)

Связанное