Referința comenzilor
Fiecare comandă e un JSON trimis cu execute. Răspunsul vine o singură dată, tot ca JSON.
Datele comenzilor sunt aceleași ca în API-ul local al terminalului: sale.create primește exact corpul lui POST /api/sales.
Toate comenzile, cu cereri în Kotlin și exemple de răspuns, sunt și în referința API, în tabul „Integrare Android”.
Cererea
{
"v": 1,
"requestId": "7f3c…",
"idempotencyKey": "b2e1…",
"command": "sale.create",
"payload": { }
}
| Câmp | Ce e |
|---|---|
v | Versiunea contractului. Azi 1. |
requestId | Identificatorul acestei trimiteri. Îl folosești la cancel și îl regăsești în evenimente. |
idempotencyKey | Cheia comenzii. Obligatorie la comenzile care schimbă ceva (vezi tabelul). SDK-ul generează una dacă nu dai tu. |
command | Numele comenzii. |
payload | Datele comenzii. La comenzile de citire devin parametrii de căutare. |
Răspunsul
{
"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șierrorsunt mereu în răspuns. La eroare,errorarecodeșimessage; codurile sunt mai jos.httpStatus,commandșireplayedlipsesc din erorile date de SDK și de legătura cu POSfix (POSFIX_*,INVALID_RESPONSE,ENGINE_NOT_READY,RESPONSE_TOO_LARGE,INTERNAL_ERROR), deci le citești cuopt….replayed: trueînseamnă că răspunsul vine din jurnal: comanda cu aceeași cheie fusese deja executată.
Comenzile
| Comandă | Ce face | Cheie obligatorie |
|---|---|---|
status | Starea terminalului și dacă POSfix e activat | nu |
sale.create | Vânzare fiscală: bon la SFS, plata, tipărirea | da |
sale.list | Vânzările, filtrate după dată, stare, casier | nu |
sale.get | O vânzare cu articolele ei (payload.id) | nu |
sale.reprint | Tipărește din nou bonul: originalul, dacă n-a ieșit, altfel un singur DUPLICAT (payload.id) | da |
cash.in / cash.out | Introducere / extragere de numerar, cu bon de serviciu | da |
report.x / report.z | Raportul X / raportul Z. Z închide și ziua bancară | da |
report.periodic | Raport periodic pe un interval | da |
fiscal.operations | Jurnalul operațiunilor fiscale | nu |
fiscal.dailySummary | Totalurile zilei fiscale curente | nu |
print.text | Text nefiscal la imprimantă | da |
card.void | Anulează o plată cu cardul, înainte de Z | da |
card.refund | Rambursează o plată cu cardul, după Z | da |
catalog.products / catalog.categories / catalog.health | Catalogul terminalului | nu |
reference.vatRates / reference.cashiers | Cotele TVA / casierii | nu |
loyalty.identify | Clientul de fidelitate, după telefon sau card | nu |
request.result | Răspunsul unei comenzi trimise înainte, după cheia ei | da |
status
Răspunde mereu, și pe un terminal neactivat.
{
"apiVersion": 1,
"activated": true,
"headlessMode": true,
"terminal": {
"fiscalStatus": "ready",
"hasOpenShift": true,
"hasCardTerminal": true,
"version": "1.0.34+42"
}
}
terminal și headlessMode lipsesc cât POSfix nu e activat. hasCardTerminal spune dacă se poate plăti cu cardul pe acest aparat.
sale.create
O vânzare de 90 lei: două articole, plătite 50 lei cash și 40 lei cu cardul.
{
"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: litera cotei TVA configurate pe terminal. Lista o dăreference.vatRates.payments[].type: codul ACPS al metodei —1numerar,2card,5tichete de masă,7alt instrument. Lista completă e în API-ul local.amountReceivedtrebuie să fie egal cu suma plăților. Pentru rest, trimite banii primiți în numerar, iar POSfix calculează restul.- Plata
2sau5fărărrnpornește terminalul de card. Currncompletat, POSfix consideră plata deja făcută în altă parte. Anularea și rambursarea ei le faci tot acolo:card.voidșicard.refundcaută doar printre plățile încasate de POSfix. cartDiscount:{ "percent": 10 }sau{ "absolute": 5.00 }pe tot bonul.loyalty:{ "memberId": "…" }dinloyalty.identify. Vânzarea adună punctele clientului.
Răspunsul are, între altele: saleId, receiptNumber, reportNumber, fiscalCode, total, change, vatBreakdown, paymentBreakdown și cardPaymentResult (rrn, authCode, cardMask, chequeNumber, isMiaQr).
Păstrează chequeNumber și rrn: primul îți trebuie la card.void, al doilea la card.refund. chequeNumber poate lipsi, fiindcă nu toate băncile îl întorc.
card.void și 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 } }
Anularea merge până la raportul Z, rambursarea după el, iar amount poate fi și o parte din plată. Niciuna nu emite bon fiscal. Dacă terminalul nu cunoaște cecul sau RRN-ul, răspunsul e NOT_FOUND.
Comenzile de citire
La sale.list, fiscal.operations și catalog.*, câmpurile din payload devin parametrii de căutare ai rutei locale:
{ "command": "sale.list", "payload": { "dateFrom": "2026-09-28", "limit": 20 } }
loyalty.identify
{ "command": "loyalty.identify", "payload": { "phoneOrBarcode": "069123456" } }
Răspunsul are memberId, name, tierName, discountPercent, pointsBalance și bonusAvailable.
Reducerea clientului o aplici tu, în cartDiscount, ca să știi totalul exact înainte să ceri plata. La sale.create trimiți apoi loyalty.memberId, iar punctele se adună singure.
Căutarea cere internet, iar un client negăsit întoarce LOYALTY_MEMBER_NOT_FOUND.
Cheia de idempotență
Cheia te apără de bonul dublu. Folosește o cheie nouă pentru fiecare vânzare nouă și aceeași cheie când repeți aceeași vânzare. Cheia rămâne legată de primul ei răspuns, chiar dacă e o eroare: după un card refuzat, încercarea următoare e o vânzare nouă, cu cheie nouă.
- Aceeași cheie și același
payload: primești răspunsul salvat, cureplayed: true. Bonul nu se emite a doua oară. - Aceeași cheie și alt
payload:IDEMPOTENCY_KEY_REUSE_MISMATCH. - Aceeași cheie cât prima comandă încă rulează:
REQUEST_IN_PROGRESS. Nimic nu se execută a doua oară. request.resultcu aceeași cheie: afli ce s-a întâmplat, fără să execuți nimic.REQUEST_IN_PROGRESSînseamnă că încă rulează;NOT_FOUND, că nu s-a primit nicio comandă cu cheia asta.
Cheile sunt separate pe aplicație: altă aplicație nu vede răspunsurile tale. Cu SDK-ul, posfix.requestResult(key) { … } trimite request.result.
Exemplu. Ai trimis o vânzare, iar aplicația ta a căzut înainte de răspuns. La repornire trimiți request.result cu cheia vânzării:
success: true: bonul s-a emis, nu-l mai trimite.REQUEST_IN_PROGRESS: vânzarea încă rulează, de exemplu așteaptă cardul. Întreabă din nou peste câteva secunde.NOT_FOUND: vânzarea n-a ajuns la POSfix. O poți trimite din nou, cu aceeași cheie.
Codurile de eroare
Codurile specifice integrării:
| Cod | Când apare |
|---|---|
TERMINAL_NOT_ACTIVATED | POSfix nu e activat pe acest terminal. Activează-l o dată, pe ecranul lui. |
VALIDATION_ERROR | Lipsește un câmp obligatoriu, de exemplu idempotencyKey sau payload.id. |
UNKNOWN_COMMAND | Numele comenzii nu există. |
INVALID_JSON | Cererea nu e un obiect JSON. |
SALE_CANCELLED | Vânzarea a fost oprită cu cancel înainte de bonul fiscal. Cardurile aprobate au fost anulate. |
COMMAND_TIMEOUT | Comanda rulează de peste 150 s. Continuă în POSfix: rezultatul îl afli cu request.result. |
NOT_FOUND | request.result: nicio comandă cu această cheie. card.void / card.refund: terminalul nu cunoaște cecul sau RRN-ul. |
REQUEST_IN_PROGRESS | O comandă cu aceeași cheie încă rulează. Întreabă din nou cu request.result. |
LOYALTY_MEMBER_NOT_FOUND | Clientul nu există sau serviciul de fidelitate nu răspunde. |
ENGINE_NOT_READY | POSfix n-a pornit în 45 s de la trimiterea comenzii. |
RESPONSE_TOO_LARGE | Răspunsul depășește 400 KB. Cere mai puține rânduri, cu limit și offset. |
POSFIX_NOT_CONNECTED | SDK: ai trimis o comandă înainte de connect(). |
POSFIX_DISCONNECTED | SDK: POSfix s-a oprit cât aștepta comanda. Verifică rezultatul cu request.result. |
INVALID_RESPONSE | SDK: răspunsul POSfix nu s-a putut citi. |
INTERNAL_ERROR | Eroare neprevăzută în POSfix. Verifică rezultatul cu request.result înainte să retrimiți. |
Codurile care vin din API-ul local și apar cel mai des:
| Cod | Când apare |
|---|---|
FISCAL_DAY_EXPIRED | Ziua fiscală a expirat: trimite report.z. |
TERMINAL_NOT_FISCAL | Terminalul nu e încă fiscalizat. |
BUSY | Altă operațiune fiscală e în curs. Reîncearcă peste câteva secunde, cu aceeași cheie. |
CARD_PAYMENT_DECLINED | Banca a refuzat plata sau clientul a renunțat pe terminal. |
CARD_PAYMENT_ERROR | Plata cu cardul a eșuat din alt motiv decât refuzul băncii. |
CARD_TERMINAL_UNAVAILABLE | Pe acest aparat nu se poate plăti cu cardul. |
DUPLICATE_LIMIT_REACHED | Duplicatul bonului a fost deja tipărit. |
IDEMPOTENCY_KEY_REUSE_MISMATCH | Aceeași cheie, trimisă cu alt payload. |
MEV_ERROR / MEV_TIMEOUT / MEV_UNAVAILABLE | SFS a refuzat bonul sau nu a răspuns. |
Restul sunt în Referința API.
Fără SDK
Dacă nu vrei dependența SDK-ului, folosești direct interfața AIDL:
- Descarcă fișierele AIDL și dezarhivează-le în
src/main/aidl/. - În manifest adaugă
<queries><package android:name="md.posfix.posfix" /></queries>. Fără el, pe Android 11+ legarea întoarcefalse. - Leagă-te cu
Intent("md.posfix.integration.BIND").setPackage("md.posfix.posfix")șiBIND_AUTO_CREATE. Pe Android 14+ adaugă șiBIND_ALLOW_ACTIVITY_STARTS, altfel POSfix nu poate deschide ecranul de plată al băncii. - Apelezi
execute(requestJson, callback). E un apel asincron: rezultatul vine peIPosfixCallback.onResult.
Pe lângă execute, interfața are getApiVersion(), cancel(requestId) și registerListener / unregisterListener pentru evenimente.