Справочник команд
Каждая команда — это JSON, отправленный через execute. Ответ приходит один раз, тоже в виде JSON.
Данные команд те же, что и в локальном API терминала: sale.create принимает ровно то же тело, что и POST /api/sales.
Все команды, с запросами на Kotlin и примерами ответов, есть также в справочнике API, на вкладке «Интеграция Android».
Запрос
{
"v": 1,
"requestId": "7f3c…",
"idempotencyKey": "b2e1…",
"command": "sale.create",
"payload": { }
}
| Поле | Что это |
|---|---|
v | Версия контракта. Сейчас 1. |
requestId | Идентификатор этой отправки. Вы используете его в cancel и находите в событиях. |
idempotencyKey | Ключ команды. Обязателен для команд, которые что-то изменяют (см. таблицу). Если вы его не передали, SDK создаст ключ сам. |
command | Имя команды. |
payload | Данные команды. В командах чтения они становятся параметрами поиска. |
Ответ
{
"v": 1,
"requestId": "7f3c…",
"command": "sale.create",
"httpStatus": 200,
"replayed": false,
"success": true,
"data": { "saleId": "…", "receiptNumber": 42, "fiscalCode": "…" },
"error": null,
"timestamp": "2026-09-28T13:12:23.076Z"
}
successиerrorесть в ответе всегда. При ошибке вerrorестьcodeиmessage; коды приведены ниже.httpStatus,commandиreplayedотсутствуют в ошибках, которые возвращают SDK и связь с POSfix (POSFIX_*,INVALID_RESPONSE,ENGINE_NOT_READY,RESPONSE_TOO_LARGE,INTERNAL_ERROR), поэтому читайте их черезopt….replayed: trueозначает, что ответ взят из журнала: команда с тем же ключом уже была выполнена.
Команды
| Команда | Что делает | Ключ обязателен |
|---|---|---|
status | Состояние терминала и активирован ли POSfix | нет |
sale.create | Фискальная продажа: чек в SFS, оплата, печать | да |
sale.list | Продажи с фильтром по дате, статусу, кассиру | нет |
sale.get | Одна продажа с её позициями (payload.id) | нет |
sale.reprint | Печатает чек повторно: оригинал, если он не вышел, иначе копию DUPLICAT, только один раз (payload.id) | да |
cash.in / cash.out | Внесение / выдача наличных с сервисным чеком | да |
report.x / report.z | X-отчёт / Z-отчёт. Z закрывает и банковский день | да |
report.periodic | Периодический отчёт за интервал | да |
fiscal.operations | Журнал фискальных операций | нет |
fiscal.dailySummary | Итоги текущего фискального дня | нет |
print.text | Нефискальный текст на принтер | да |
card.void | Отменяет оплату картой до Z-отчёта | да |
card.refund | Возвращает оплату картой после Z-отчёта | да |
catalog.products / catalog.categories / catalog.health | Каталог терминала | нет |
reference.vatRates / reference.cashiers | Ставки НДС / кассиры | нет |
loyalty.identify | Клиент программы лояльности по телефону или карте | нет |
request.result | Ответ на ранее отправленную команду по её ключу | да |
status
Отвечает всегда, даже на неактивированном терминале.
{
"apiVersion": 1,
"activated": true,
"headlessMode": true,
"terminal": {
"fiscalStatus": "ready",
"hasOpenShift": true,
"hasCardTerminal": true,
"version": "1.0.34+42"
}
}
terminal и headlessMode отсутствуют, пока POSfix не активирован. hasCardTerminal показывает, можно ли на этом устройстве платить картой.
sale.create
Продажа на 90 леев: две позиции, оплачено 50 леев наличными и 40 леев картой.
{
"items": [
{ "name": "Pizza Margherita", "unitPrice": 70.00, "quantity": 1, "vatCode": "B" },
{ "name": "Apă plată 0,5 l", "unitPrice": 10.00, "quantity": 2, "vatCode": "B" }
],
"payments": [
{ "type": "1", "amount": 50.00 },
{ "type": "2", "amount": 40.00 }
],
"amountReceived": 90.00,
"printReceipt": true
}
vatCode: буква ставки НДС, настроенной на терминале. Список выдаётreference.vatRates.payments[].type: код способа оплаты по ACPS —1наличные,2карта,5талоны на питание,7другой платёжный инструмент. Полный список — в локальном API.amountReceivedдолжен быть равен сумме оплат. Если нужна сдача, передайте сумму, полученную наличными, а POSfix рассчитает сдачу.- Оплата
2или5безrrnзапускает карточный терминал. Еслиrrnзаполнен, POSfix считает, что оплата уже проведена в другом месте. Отмену и возврат тоже выполняйте там:card.voidиcard.refundищут только среди оплат, принятых POSfix. cartDiscount:{ "percent": 10 }или{ "absolute": 5.00 }на весь чек.loyalty:{ "memberId": "…" }изloyalty.identify. Продажа начисляет клиенту баллы.
В ответе, среди прочего, есть: saleId, receiptNumber, reportNumber, fiscalCode, total, change, vatBreakdown, paymentBreakdown и cardPaymentResult (rrn, authCode, cardMask, chequeNumber, isMiaQr).
Сохраните chequeNumber и rrn: первый понадобится для card.void, второй — для card.refund. chequeNumber может отсутствовать, потому что его возвращают не все банки.
card.void и card.refund
{ "command": "card.void", "payload": { "chequeNumber": "000123", "amount": 40.00, "reason": "Clientul a renunțat" } }
{ "command": "card.refund", "payload": { "originalRrn": "627110323023", "amount": 40.00 } }
Отмена возможна до Z-отчёта, возврат — после него, а amount может быть и частью оплаты. Ни та, ни другая операция не выдаёт фискальный чек. Если терминалу неизвестен такой чек или RRN, ответ — NOT_FOUND.
Команды чтения
В sale.list, fiscal.operations и catalog.* поля из payload становятся параметрами поиска локального маршрута:
{ "command": "sale.list", "payload": { "dateFrom": "2026-09-28", "limit": 20 } }
loyalty.identify
{ "command": "loyalty.identify", "payload": { "phoneOrBarcode": "069123456" } }
В ответе есть memberId, name, tierName, discountPercent, pointsBalance и bonusAvailable.
Скидку клиента применяете вы сами, в cartDiscount, чтобы знать точную сумму до того, как запросите оплату. Затем передайте loyalty.memberId в sale.create, и баллы начислятся сами.
Поиск работает только при наличии интернета. Если клиент не найден, возвращается LOYALTY_MEMBER_NOT_FOUND.
Ключ идемпотентности
Ключ защищает вас от двойного чека. Используйте новый ключ для каждой новой продажи и тот же ключ, когда повторяете ту же продажу. Ключ остаётся привязан к своему первому ответу, даже если это ошибка: после отклонённой карты следующая попытка — это новая продажа с новым ключом.
- Тот же ключ и тот же
payload: вы получаете сохранённый ответ сreplayed: true. Чек не выдаётся второй раз. - Тот же ключ и другой
payload:IDEMPOTENCY_KEY_REUSE_MISMATCH. - Тот же ключ, пока первая команда ещё выполняется:
REQUEST_IN_PROGRESS. Ничего не выполняется второй раз. request.resultс тем же ключом: вы узнаёте, что произошло, ничего не выполняя.REQUEST_IN_PROGRESSозначает, что команда ещё выполняется;NOT_FOUND— что команда с этим ключом не поступала.
Ключи разделены по приложениям: другое приложение не видит ваших ответов. В SDK вызов posfix.requestResult(key) { … } отправляет request.result.
Пример. Вы отправили продажу, а ваше приложение упало до ответа. После перезапуска отправьте request.result с ключом продажи:
success: true: чек выдан, не отправляйте продажу снова.REQUEST_IN_PROGRESS: продажа ещё выполняется, например ждёт карту. Спросите снова через несколько секунд.NOT_FOUND: продажа не дошла до POSfix. Её можно отправить снова, с тем же ключом.
Коды ошибок
Коды, специфичные для интеграции:
| Код | Когда возникает |
|---|---|
TERMINAL_NOT_ACTIVATED | POSfix не активирован на этом терминале. Активируйте его один раз, на его экране. |
VALIDATION_ERROR | Не хватает обязательного поля, например idempotencyKey или payload.id. |
UNKNOWN_COMMAND | Команды с таким именем не существует. |
INVALID_JSON | Запрос не является JSON-объектом. |
SALE_CANCELLED | Продажа остановлена через cancel до выдачи фискального чека. Одобренные оплаты картой отменены. |
COMMAND_TIMEOUT | Команда выполняется дольше 150 с. Она продолжается в POSfix: результат вы узнаете через request.result. |
NOT_FOUND | request.result: нет команды с этим ключом. card.void / card.refund: терминалу неизвестен такой чек или RRN. |
REQUEST_IN_PROGRESS | Команда с тем же ключом ещё выполняется. Спросите снова через request.result. |
LOYALTY_MEMBER_NOT_FOUND | Клиент не существует или сервис программы лояльности не отвечает. |
ENGINE_NOT_READY | POSfix не запустился за 45 с после отправки команды. |
RESPONSE_TOO_LARGE | Ответ больше 400 КБ. Запрашивайте меньше строк с помощью limit и offset. |
POSFIX_NOT_CONNECTED | SDK: вы отправили команду до connect(). |
POSFIX_DISCONNECTED | SDK: POSfix остановился, пока команда ждала ответа. Проверьте результат через request.result. |
INVALID_RESPONSE | SDK: не удалось прочитать ответ POSfix. |
INTERNAL_ERROR | Непредвиденная ошибка в POSfix. Проверьте результат через request.result, прежде чем отправлять команду повторно. |
Коды, которые приходят из локального API и встречаются чаще всего:
| Код | Когда возникает |
|---|---|
FISCAL_DAY_EXPIRED | Фискальный день истёк: отправьте report.z. |
TERMINAL_NOT_FISCAL | Терминал ещё не фискализирован. |
BUSY | Идёт другая фискальная операция. Повторите через несколько секунд с тем же ключом. |
CARD_PAYMENT_DECLINED | Банк отклонил оплату, или клиент отказался от неё на терминале. |
CARD_PAYMENT_ERROR | Оплата картой не прошла по другой причине, не из-за отказа банка. |
CARD_TERMINAL_UNAVAILABLE | На этом устройстве нельзя платить картой. |
DUPLICATE_LIMIT_REACHED | Дубликат чека уже напечатан. |
IDEMPOTENCY_KEY_REUSE_MISMATCH | Тот же ключ, отправленный с другим payload. |
MEV_ERROR / MEV_TIMEOUT / MEV_UNAVAILABLE | SFS отклонила чек или не ответила. |
Остальные — в Справочнике API.
Без SDK
Если вы не хотите зависеть от SDK, используйте интерфейс AIDL напрямую:
- Скачайте файлы AIDL и распакуйте их в
src/main/aidl/. - Добавьте в манифест
<queries><package android:name="md.posfix.posfix" /></queries>. Без этого на Android 11+ привязка возвращаетfalse. - Привяжитесь через
Intent("md.posfix.integration.BIND").setPackage("md.posfix.posfix")сBIND_AUTO_CREATE. На Android 14+ добавьте такжеBIND_ALLOW_ACTIVITY_STARTS, иначе POSfix не сможет открыть платёжный экран банка. - Вызовите
execute(requestJson, callback). Это асинхронный вызов: результат приходит вIPosfixCallback.onResult.
Кроме execute, в интерфейсе есть getApiVersion(), cancel(requestId) и registerListener / unregisterListener для событий.