Skip to main content

Command reference

Each command is a JSON object sent with execute. The response comes back exactly once, also as JSON. The command data is the same as in the terminal's local API: sale.create takes exactly the body of POST /api/sales. Every command, with Kotlin requests and example responses, is also in the API reference, on the “Android integration” tab.

The request​

{
"v": 1,
"requestId": "7f3c…",
"idempotencyKey": "b2e1…",
"command": "sale.create",
"payload": { }
}
FieldWhat it is
vThe contract version. Today it is 1.
requestIdThe ID of this send. You use it with cancel, and you find it again in the events.
idempotencyKeyThe command key. Required for commands that change something (see the table). The SDK generates one if you don't pass it.
commandThe command name.
payloadThe command data. For read commands, it becomes the search parameters.

The response​

{
"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 and error are always in the response. On an error, error has code and message; the codes are below.
  • httpStatus, command and replayed are missing from the errors that come from the SDK and from the connection to POSfix (POSFIX_*, INVALID_RESPONSE, ENGINE_NOT_READY, RESPONSE_TOO_LARGE, INTERNAL_ERROR), so you read them with opt….
  • replayed: true means the response comes from the log: a command with the same key had already run.

The commands​

CommandWhat it doesKey required
statusThe terminal state and whether POSfix is activatedno
sale.createFiscal sale: receipt to SFS, payment, printingyes
sale.listSales, filtered by date, state, cashierno
sale.getOne sale with its items (payload.id)no
sale.reprintPrints the receipt again: the original if it did not come out, otherwise a single DUPLICATE (payload.id)yes
cash.in / cash.outCash in / cash out, with a service receiptyes
report.x / report.zThe X report / the Z report. Z also closes the banking dayyes
report.periodicPeriodic report for a date rangeyes
fiscal.operationsThe log of fiscal operationsno
fiscal.dailySummaryThe totals of the current fiscal dayno
print.textNon-fiscal text to the printeryes
card.voidVoids a card payment, before the Z reportyes
card.refundRefunds a card payment, after the Z reportyes
catalog.products / catalog.categories / catalog.healthThe terminal's catalogno
reference.vatRates / reference.cashiersVAT rates / cashiersno
loyalty.identifyThe loyalty customer, by phone or cardno
request.resultThe response to a command sent earlier, by its keyyes

status​

It always answers, even on a terminal that is not activated.

{
"apiVersion": 1,
"activated": true,
"headlessMode": true,
"terminal": {
"fiscalStatus": "ready",
"hasOpenShift": true,
"hasCardTerminal": true,
"version": "1.0.34+42"
}
}

terminal and headlessMode are missing while POSfix is not activated. hasCardTerminal tells you whether this device can take card payments.

sale.create​

A 90 lei sale: two items, paid 50 lei in cash and 40 lei by card.

{
"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: the letter of a VAT rate configured on the terminal. reference.vatRates gives you the list.
  • payments[].type: the ACPS code of the payment method — 1 cash, 2 card, 5 meal vouchers, 7 other payment instrument. The full list is in the local API.
  • amountReceived must equal the sum of the payments. To give change, send the cash you received, and POSfix calculates the change.
  • A payment of type 2 or 5 without rrn starts the card terminal. With rrn filled in, POSfix treats the payment as already made elsewhere. Void and refund it there too: card.void and card.refund look only among the payments POSfix collected.
  • cartDiscount: { "percent": 10 } or { "absolute": 5.00 } on the whole receipt.
  • loyalty: { "memberId": "…" } from loyalty.identify. The sale adds points to the customer's balance.

The response includes, among others: saleId, receiptNumber, reportNumber, fiscalCode, total, change, vatBreakdown, paymentBreakdown and cardPaymentResult (rrn, authCode, cardMask, chequeNumber, isMiaQr). Keep chequeNumber and rrn: you need the first for card.void and the second for card.refund. chequeNumber can be missing, because not all banks return it.

card.void and 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 } }

A void works until the Z report, a refund after it, and amount can also be just part of the payment. Neither one issues a fiscal receipt. If the terminal does not know the bank slip number or the RRN, the response is NOT_FOUND.

Read commands​

For sale.list, fiscal.operations and catalog.*, the fields in payload become the search parameters of the local route:

{ "command": "sale.list", "payload": { "dateFrom": "2026-09-28", "limit": 20 } }

loyalty.identify​

{ "command": "loyalty.identify", "payload": { "phoneOrBarcode": "069123456" } }

The response has memberId, name, tierName, discountPercent, pointsBalance and bonusAvailable. You apply the customer's discount yourself, in cartDiscount, so you know the exact total before you ask for payment. Then you send loyalty.memberId with sale.create, and the points are added on their own. The lookup needs an internet connection, and a customer who is not found returns LOYALTY_MEMBER_NOT_FOUND.

The idempotency key​

The key protects you from a duplicate receipt. Use a new key for each new sale, and the same key when you repeat the same sale. The key stays tied to its first response, even if that response is an error: after a declined card, the next attempt is a new sale, with a new key.

  • Same key and same payload: you get the saved response, with replayed: true. The receipt is not issued a second time.
  • Same key and a different payload: IDEMPOTENCY_KEY_REUSE_MISMATCH.
  • Same key while the first command is still running: REQUEST_IN_PROGRESS. Nothing runs a second time.
  • request.result with the same key: you find out what happened, without running anything. REQUEST_IN_PROGRESS means the command is still running; NOT_FOUND means no command with this key was received.

Keys are separate for each app: another app does not see your responses. With the SDK, posfix.requestResult(key) { … } sends request.result.

Example. You sent a sale, and your app crashed before the response. When it restarts, you send request.result with the sale's key:

  • success: true: the receipt was issued. Don't send the sale again.
  • REQUEST_IN_PROGRESS: the sale is still running, for example it is waiting for the card. Ask again in a few seconds.
  • NOT_FOUND: the sale did not reach POSfix. You can send it again, with the same key.

Error codes​

The codes specific to the integration:

CodeWhen it appears
TERMINAL_NOT_ACTIVATEDPOSfix is not activated on this terminal. Activate it once, on its screen.
VALIDATION_ERRORA required field is missing, for example idempotencyKey or payload.id.
UNKNOWN_COMMANDThe command name does not exist.
INVALID_JSONThe request is not a JSON object.
SALE_CANCELLEDThe sale was stopped with cancel before the fiscal receipt. Approved card payments were voided.
COMMAND_TIMEOUTThe command has been running for more than 150 s. It keeps going in POSfix: you get the result with request.result.
NOT_FOUNDrequest.result: no command with this key. card.void / card.refund: the terminal does not know the bank slip number or the RRN.
REQUEST_IN_PROGRESSA command with the same key is still running. Ask again with request.result.
LOYALTY_MEMBER_NOT_FOUNDThe customer does not exist, or the loyalty service is not responding.
ENGINE_NOT_READYPOSfix did not start within 45 s after the command was sent.
RESPONSE_TOO_LARGEThe response is larger than 400 KB. Ask for fewer rows, with limit and offset.
POSFIX_NOT_CONNECTEDSDK: you sent a command before connect().
POSFIX_DISCONNECTEDSDK: POSfix stopped while the command was waiting. Check the result with request.result.
INVALID_RESPONSESDK: the POSfix response could not be read.
INTERNAL_ERRORAn unexpected error in POSfix. Check the result with request.result before you send the command again.

The codes that come from the local API and appear most often:

CodeWhen it appears
FISCAL_DAY_EXPIREDThe fiscal day has expired: send report.z.
TERMINAL_NOT_FISCALThe terminal is not fiscalized yet.
BUSYAnother fiscal operation is in progress. Retry in a few seconds, with the same key.
CARD_PAYMENT_DECLINEDThe bank declined the payment, or the customer cancelled on the terminal.
CARD_PAYMENT_ERRORThe card payment failed for a reason other than a bank decline.
CARD_TERMINAL_UNAVAILABLEThis device cannot take card payments.
DUPLICATE_LIMIT_REACHEDThe receipt's duplicate was already printed.
IDEMPOTENCY_KEY_REUSE_MISMATCHThe same key, sent with a different payload.
MEV_ERROR / MEV_TIMEOUT / MEV_UNAVAILABLESFS rejected the receipt or did not respond.

The rest are in the API reference.

Without the SDK​

If you don't want the SDK dependency, you use the AIDL interface directly:

  1. Download the AIDL files and unzip them into src/main/aidl/.
  2. In the manifest, add <queries><package android:name="md.posfix.posfix" /></queries>. Without it, binding returns false on Android 11+.
  3. Bind with Intent("md.posfix.integration.BIND").setPackage("md.posfix.posfix") and BIND_AUTO_CREATE. On Android 14+, also add BIND_ALLOW_ACTIVITY_STARTS, otherwise POSfix cannot open the bank's payment screen.
  4. You call execute(requestJson, callback). The call is asynchronous: the result comes on IPosfixCallback.onResult.

Besides execute, the interface has getApiVersion(), cancel(requestId) and registerListener / unregisterListener for events.