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": { }
}
| Field | What it is |
|---|---|
v | The contract version. Today it is 1. |
requestId | The ID of this send. You use it with cancel, and you find it again in the events. |
idempotencyKey | The command key. Required for commands that change something (see the table). The SDK generates one if you don't pass it. |
command | The command name. |
payload | The 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"
}
successanderrorare always in the response. On an error,errorhascodeandmessage; the codes are below.httpStatus,commandandreplayedare 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 withopt….replayed: truemeans the response comes from the log: a command with the same key had already run.
The commands
| Command | What it does | Key required |
|---|---|---|
status | The terminal state and whether POSfix is activated | no |
sale.create | Fiscal sale: receipt to SFS, payment, printing | yes |
sale.list | Sales, filtered by date, state, cashier | no |
sale.get | One sale with its items (payload.id) | no |
sale.reprint | Prints the receipt again: the original if it did not come out, otherwise a single DUPLICATE (payload.id) | yes |
cash.in / cash.out | Cash in / cash out, with a service receipt | yes |
report.x / report.z | The X report / the Z report. Z also closes the banking day | yes |
report.periodic | Periodic report for a date range | yes |
fiscal.operations | The log of fiscal operations | no |
fiscal.dailySummary | The totals of the current fiscal day | no |
print.text | Non-fiscal text to the printer | yes |
card.void | Voids a card payment, before the Z report | yes |
card.refund | Refunds a card payment, after the Z report | yes |
catalog.products / catalog.categories / catalog.health | The terminal's catalog | no |
reference.vatRates / reference.cashiers | VAT rates / cashiers | no |
loyalty.identify | The loyalty customer, by phone or card | no |
request.result | The response to a command sent earlier, by its key | yes |
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.vatRatesgives you the list.payments[].type: the ACPS code of the payment method —1cash,2card,5meal vouchers,7other payment instrument. The full list is in the local API.amountReceivedmust equal the sum of the payments. To give change, send the cash you received, and POSfix calculates the change.- A payment of type
2or5withoutrrnstarts the card terminal. Withrrnfilled in, POSfix treats the payment as already made elsewhere. Void and refund it there too:card.voidandcard.refundlook only among the payments POSfix collected. cartDiscount:{ "percent": 10 }or{ "absolute": 5.00 }on the whole receipt.loyalty:{ "memberId": "…" }fromloyalty.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, withreplayed: 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.resultwith the same key: you find out what happened, without running anything.REQUEST_IN_PROGRESSmeans the command is still running;NOT_FOUNDmeans 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:
| Code | When it appears |
|---|---|
TERMINAL_NOT_ACTIVATED | POSfix is not activated on this terminal. Activate it once, on its screen. |
VALIDATION_ERROR | A required field is missing, for example idempotencyKey or payload.id. |
UNKNOWN_COMMAND | The command name does not exist. |
INVALID_JSON | The request is not a JSON object. |
SALE_CANCELLED | The sale was stopped with cancel before the fiscal receipt. Approved card payments were voided. |
COMMAND_TIMEOUT | The command has been running for more than 150 s. It keeps going in POSfix: you get the result with request.result. |
NOT_FOUND | request.result: no command with this key. card.void / card.refund: the terminal does not know the bank slip number or the RRN. |
REQUEST_IN_PROGRESS | A command with the same key is still running. Ask again with request.result. |
LOYALTY_MEMBER_NOT_FOUND | The customer does not exist, or the loyalty service is not responding. |
ENGINE_NOT_READY | POSfix did not start within 45 s after the command was sent. |
RESPONSE_TOO_LARGE | The response is larger than 400 KB. Ask for fewer rows, with limit and offset. |
POSFIX_NOT_CONNECTED | SDK: you sent a command before connect(). |
POSFIX_DISCONNECTED | SDK: POSfix stopped while the command was waiting. Check the result with request.result. |
INVALID_RESPONSE | SDK: the POSfix response could not be read. |
INTERNAL_ERROR | An 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:
| Code | When it appears |
|---|---|
FISCAL_DAY_EXPIRED | The fiscal day has expired: send report.z. |
TERMINAL_NOT_FISCAL | The terminal is not fiscalized yet. |
BUSY | Another fiscal operation is in progress. Retry in a few seconds, with the same key. |
CARD_PAYMENT_DECLINED | The bank declined the payment, or the customer cancelled on the terminal. |
CARD_PAYMENT_ERROR | The card payment failed for a reason other than a bank decline. |
CARD_TERMINAL_UNAVAILABLE | This device cannot take card payments. |
DUPLICATE_LIMIT_REACHED | The receipt's duplicate was already printed. |
IDEMPOTENCY_KEY_REUSE_MISMATCH | The same key, sent with a different payload. |
MEV_ERROR / MEV_TIMEOUT / MEV_UNAVAILABLE | SFS 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:
- Download the AIDL files and unzip them into
src/main/aidl/. - In the manifest, add
<queries><package android:name="md.posfix.posfix" /></queries>. Without it, binding returnsfalseon Android 11+. - Bind with
Intent("md.posfix.integration.BIND").setPackage("md.posfix.posfix")andBIND_AUTO_CREATE. On Android 14+, also addBIND_ALLOW_ACTIVITY_STARTS, otherwise POSfix cannot open the bank's payment screen. - You call
execute(requestJson, callback). The call is asynchronous: the result comes onIPosfixCallback.onResult.
Besides execute, the interface has getApiVersion(), cancel(requestId) and registerListener / unregisterListener for events.