Documentație · Poleo Fiscal · Manual API
Manualul API-ului Poleo Fiscal
Orice program care știe să gestioneze o cerere HTTP poate tipări bonuri fiscale, cere rapoarte și citi evidența casei de marcat. Fără fișiere pe disc, fără protocol de ghicit.
Ce este API-ul
Poleo Fiscal stă între programul tău de vânzare și casa de marcat. Până acum se putea vorbi cu el în două feluri: lăsând fișiere într-un folder urmărit, sau prin serverul nostru de tipărire, cu protocol propriu. Amândouă cer acces la calculatorul din magazin.
API-ul este a treia cale, și cea mai simplă: trimiți un JSON, primești pe loc numărul bonului fiscal. Funcționează cu orice limbaj, de pe orice calculator care ajunge la stație, inclusiv dintr-un site sau dintr-o aplicație de comenzi.
Bonul trimis prin API trece prin exact aceleași reguli ca unul trimis prin fișier: aceeași verificare a sumelor, aceeași rotunjire la ban, aceleași reguli de reducere. Nu este o a doua cale cu alt comportament ci o altă ușă spre același drum.
Pornire în 3 pași
- Pornește API-ul. În aplicație: Setări → API, bifează „Pornește API-ul HTTP". Implicit ascultă doar pe calculatorul pe care rulează (
127.0.0.1). Dacă programul tău este pe alt calculator, bifează și „Ascultă în rețea". - Copiază tokenul. Tot în Setări → API. Se generează singur la prima pornire și rămâne același până îl schimbi tu.
- Verifică legătura. Cere
/api/v1/health- dacă îți răspunde, restul e doar chestiune de a trimite JSON-ul potrivit.
curl http://127.0.0.1:9111/api/v1/health
Răspuns 200 cu {"ok": true, "service": "poleo-fiscal", "api": "v1"}. Atât - dinadins nu spune nimic despre casă cât timp nu te-ai autentificat. Dacă primești „connection refused", API-ul nu este pornit din Setări sau ai greșit portul.
Configurarea Poleo, pe scurt
Înainte de prima linie de cod, merită să știi ce se configurează o singură dată, de un om, ce poți citi din API ca să te potrivești pe stația clientului, și ce poți schimba singur.
1. Ce face omul în aplicație, o dată
- Casa de marcat: marca, modelul și conexiunea (COM, TCP/IP sau Bluetooth). Aici se leagă și NUI-ul - seria aparatului, cu care îl vei individualiza tu în cereri.
- API-ul: Setări → API - pornit, portul (implicit 9111), „Ascultă în rețea" dacă programul tău este pe alt calculator, tokenul, și adresele permise din browser (CORS) dacă îl chemi dintr-o pagină web.
- Datele firmei: apar pe procesul-verbal de retur și pe declarația de bon greșit. Se pot completa și prin API.
- Terminalul de card, dacă se folosește, și folderul monitorizat, dacă preferi integrarea prin fișiere în locul API-ului.
2. Ce citești din API, ca să te potrivești pe stație
Nu presupune nimic despre configurația clientului - întreab-o. Sunt cinci citiri care merită făcute la pornirea integrării tale:
| Metodă | Ce afli |
|---|---|
GET /methods | Ce știe versiunea instalată. O stație mai veche poate să nu aibă ultimele metode - afli din cod, nu dintr-o eroare la client. |
GET /cases | Casele configurate și NUI-ul fiecăreia. Pe NUI individualizezi casa, nu pe denumire și nu pe id. |
GET /cases/{nui}/config | Cotele de TVA pe grupe și sloturile de încasare, citite din aparat. Astea sunt adevărul: nu presupune că 21% e mereu grupa A sau că 1 = numerar. |
GET /terminals | Terminalele de card și, în can, ce operațiuni au: retur, anulare, închidere de zi. |
GET /settings | Cum e configurată stația: bifele de bon, pragurile, identitatea. Și care dintre ele nu se pot schimba din afară, cu motivul scris. |
3. Ce poți schimba tu, din program
POST /settings pentru setările operaționale - cum arată și cum se poartă bonul, pragurile de numerar, punctul de lucru - și POST /company pentru datele firmei. Intră în vigoare pe loc, fără repornire.
Cheia și portul API-ului, parolele de email și SFTP, adresele către care pleacă evidența, raportul Z automat, bifele de backup, rutarea plăților cu cardul - toate se văd prin API, dar nu se schimbă. Motivul e simplu: cine le poate schimba dintr-un program își poate stinge singur urmele - oprește raportul de anulări către contabilitate, schimbă adresa contabilului, taie backupul, și abia apoi face ce vrea.
Regula de fond, din care ies toate astea: nimic automat dacă are implicații juridice sau fiscale. Omul alege, noi îi spunem care e problema.
4. Ordinea la prima integrare
GET /health- există serviciul? (nu cere token)GET /cases- ia NUI-ul casei și pune-l în fiecare cerere, lacase.GET /cases/{nui}/config- ia cotele și sloturile, și potrivește-ți nomenclatorul pe ele.POST /receiptscu cheie de operațiune - primul bon.GET /receipts?limit=1- vezi-l în evidență, cu numărul lui fiscal.
Toți pașii ăștia sunt scriși ca exemple care se rulează, în exemplele de mai jos.
Toate exemplele, gata de rulat
Daca preferi sa vezi API-ul functionand inainte sa scrii o linie de cod, ruleaza exemplele. Sunt aceleasi 45 in Python, PHP si C, numerotate la fel - iti arata cererea trimisa si raspunsul primit, si explica ce s-a intamplat. Alege limbajul care iti convine; descarcarile si cerintele fiecaruia sunt mai jos, la Clienti.
set POLEO_URL=http://127.0.0.1:9111
set POLEO_TOKEN=tokenul-tau
set POLEO_CASA=DB9999999999
python poleo_demo.py --lista # ce exemple exista
python poleo_demo.py 5 # un bon platit cu numerar
python poleo_demo.py 5 7 13 # mai multe
python poleo_demo.py toate # tot, mai putin ce este ireversibil
php poleo_demo.php --lista # acelasi catalog, in PHP
poleo_demo.exe --lista # si in C, dupa ce l-ai compilat
- Acopera toate metodele API-ului: bonuri de orice fel, rapoarte, numerar, jurnal electronic, operatiunile de card, citirile din evidenta.
- Raportul Z este marcat ca ireversibil si nu ruleaza fara
--da. Cheia lui contine ziua, exact cum trebuie facut in productie. - Fiecare exemplu are, sub el, explicatia a ce s-a intamplat si capcanele lui - este material de citit, nu doar de rulat.
- Se instaleaza si local, langa aplicatie, in
docs/api- toate trei, nu doar unul.
Clienți gata făcuți: Python, PHP și C
API-ul este HTTP și JSON, deci merge din orice limbaj care poate face o cerere web - Java, C#, Delphi, Go, Node, Rust, ce folosești deja. Cei trei clienți de mai jos sunt doar un picior de plecare, ca să nu scrii transportul de la zero: fiecare fără dependințe, cu aceleași 45 de exemple numerotate la fel - exemplul 31 din Python este exemplul 31 din PHP și din C. Dacă limbajul tău nu e printre ei, citește oricare dintre cele trei ca model: partea de transport stă la vedere, într-un singur loc, iar restul fișierului sunt exemple.
Toți se instalează și local, lângă aplicație, în docs/api.
Python
3.8+, biblioteca standard
- poleo_demo.py - clientul și exemplele
Un singur fișier, nimic de instalat.
PHP
7.4+, fără composer
- PoleoFiscal.php - clientul
- poleo_demo.php - exemplele
- poleo_web.php - portal minimal
Fără framework. Merge și sub XAMPP.
C
zero dependințe pe Windows
- poleo_fiscal.h - interfața
- poleo_fiscal.c - implementarea
- poleo_demo.c - exemplele
Transport pe WinHTTP. Merge și din C++.
python poleo_demo.py --lista
php poleo_demo.php --lista
gcc -std=c99 poleo_fiscal.c poleo_demo.c -lwinhttp -o poleo_demo.exe
cl /W3 poleo_fiscal.c poleo_demo.c winhttp.lib
cc -std=c99 -DPOLEO_CU_CURL poleo_fiscal.c poleo_demo.c -lcurl
- Pe Windows, clientul C nu cere nicio bibliotecă în plus:
transportul merge pe WinHTTP, care vine cu sistemul. Pe Linux și macOS se
compilează cu
-DPOLEO_CU_CURL. Merge și din C++, areextern "C". - Cititorul de JSON din clientul C nu este un parser complet, si
nici nu incearca sa fie: scoate un camp dupa nume, cat sa iei
receipt_no,idsaustatus. Pentru liste, ia raspunsul brut si da-l parserului pe care il folosesti deja - nu-ti impunem noi o dependinta. - Explicatiile lungi - de ce, ce se strica daca gresesti, ce raspunde casa - stau in fisierul Python si in manualul asta. In PHP si C sunt note scurte, ca sa nu tinem aceeasi proza in trei locuri si sa ajunga sa se contrazica.
API-ul ascultă implicit doar pe calculatorul lui, și opțional în rețeaua locală. Un portal găzduit în internet nu ajunge de la sine la casa din magazin. De aici pleacă două situații care se confundă des, și au răspunsuri opuse:
1. Casierul stă la tejghea (portal de casierie, POS în cloud).
Browserul lui este în aceeași rețea cu casa, deci el poate chema
http://127.0.0.1:9111 direct. Treci adresa portalului în
Setări → API → Origini permise și serverul tău nu atinge casa deloc.
Fișierul poleo_web.php arată această așezare, lângă cea obișnuită.
2. Clientul comandă de acasă, iar bonul se tipărește în magazin (magazin online). Aici niciun browser nu e în magazin, deci comanda pleacă din backendul tău - și pentru asta îți trebuie o cale până la calculatorul din magazin. Vezi Cum leg un site la casa de marcat.
Ce nu se face în niciunul dintre cazuri: nu deschizi portul API direct în internet. Iar tokenul, în varianta din browser, se dă doar utilizatorului logat care are dreptul să tipărească - niciodată într-o pagină publică.
Autentificare
Fiecare stație are tokenul ei. Îl trimiți la fiecare cerere, în antetul
Authorization:
Authorization: Bearer TOKENUL_DIN_SETARI
Dacă îți este mai la îndemână, merge și antetul X-Poleo-Token cu aceeași valoare.
Cine îl are poate tipări bonuri fiscale pe aparatul tău. Nu-l pune în JavaScript-ul unui site public și nu-l trimite pe email. Dacă bănuiești că a ajuns unde nu trebuie, generează altul din Setări - cel vechi devine inutil în aceeași clipă.
Fără token, sau cu unul greșit, orice cale în afară de /health întoarce 401 și {"error": {"code": "unauthorized", …}}. Nu-ți spune dacă tokenul este „aproape bun" - nu se ghicește din răspuns.
Cum arată răspunsurile
Toate răspunsurile sunt JSON și au aceeași formă, ca să nu scrii cod diferit pentru fiecare cale. Când a mers:
{ "ok": true, "receipt_no": "958", "message": "OK" }
Când n-a mers:
{ "ok": false, "error": { "code": "device_error", "message": "Nu mai este hârtie în casa de marcat." } }
Verifică code, nu message. Codul este stabil și nu se schimbă; mesajul este scris pentru om și îl putem îmbunătăți oricând.
| Cod HTTP | Când |
|---|---|
200 |
A mers. |
400 |
Cererea ta este greșită - lipsește un câmp, o sumă este negativă, JSON-ul este stricat. |
401 |
Token lipsă sau greșit. |
404 |
Calea sau casa nu există. |
405 |
Ai folosit GET pe o operațiune care cere POST. |
413 |
Cererea sau conținutul cerut depășesc limitele. |
422 |
Cererea este validă, dar casa a refuzat: fără hârtie, zi neînchisă, capac deschis. |
500 |
Ceva la noi. Dacă se repetă, trimite-ne un mesaj cu timestampul, NUI-ul și problema. Dacă nu ai dezactivat raportarea erorilor către noi, posibil să putem identifica eroarea pe serverul nostru. |
Diferența dintre 400 și 422 este cea care contează în practică: la 400 repari cererea, la 422 te duci la casa de marcat.
GET citește, POST face
Regula este simplă și n-are excepții: ce citește din evidență este GET, ce cere aparatului este POST. GET /reports îți dă rapoartele deja tipărite; POST /reports chiar tipărește unul. Nu poți închide ziua fiscală din greșeală, crezând că te uiți la ea.
Cheia de operațiune
Este cea mai importantă parte a manualului, chiar dacă este cea mai plictisitoare.
Rețeaua pică exact în secunda ulterioară mesajului „casa a tipărit bonul" și „răspunsul a ajuns înapoi la tine". Programul tău nu știe ce s-a întâmplat. Dacă reîncearcă, un API obișnuit tipărește al doilea bon fiscal - pe care nu-l mai anulează nimeni. Un bon fiscal închis nu se anulează; se întocmește o declarație de bon greșit, cu aprobare și cu document - procedura este scrisă mai jos.
De aceea, la orice operațiune care lasă urmă, trimite antetul Idempotency-Key cu o valoare a ta, unică pentru acea operațiune (numărul comenzii, de exemplu). La o a doua cerere cu aceeași cheie nu se mai execută nimic: îți întoarcem rezultatul primei.
curl -X POST http://127.0.0.1:9111/api/v1/receipts \
-H "Authorization: Bearer TOKENUL" \
-H "Idempotency-Key: comanda-4471" \
-H "Content-Type: application/json" \
-d '{"receipt": {"items": [...], "payments": [...]}}'
- Prima cerere:
200, cureceipt_no- bonul a ieșit pe hârtie. - A doua, cu aceeași cheie:
200, acelașireceipt_no, plus"repeated": true. Pe hârtie nu este tipărit nimic.
Cea mai bună cheie este ceva ce ai deja în sistemul tău și care rămâne același la o reîncercare: numărul comenzii, al facturii, al sesiunii de casă. Un timestamp sau un număr generat la fiecare încercare se schimbă între prima trimitere și a doua, iar pentru noi două chei diferite înseamnă, corect, două operațiuni diferite - deci al doilea bon.
Dacă în sistemul tău nu există un asemenea număr, poți construi cheia din datele bonului (de exemplu casa + data + numărul liniei de comandă) - important este să iasă aceeași valoare când reiei aceeași operațiune.
Ești acolo?
Singura cale care nu cere token. Serviciul spune cine e, ca să poți verifica legătura înainte să ai credențiale.
curl http://127.0.0.1:9111/api/v1/health
{ "ok": true, "service": "poleo-fiscal", "api": "v1" }
Răspunde chiar și când casa de marcat este deconectată - întrebarea este despre aplicație, nu despre aparat. Pentru starea casei ai /cases/{id}/status.
Ce case sunt configurate
Lista caselor pe care le poți comanda. De aici iei id-ul, dacă lucrezi cu mai multe aparate.
curl http://127.0.0.1:9111/api/v1/cases \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "cases": [ { "id": "01M04R5GFS7N3BAMP7JBMWTAZ4", "name": "DB9999999999",
"brand": "datecs", "model": "DP-25",
"serial": "DB9999999999", "transport": "tcp" } ] }
Dacă ai o singură casă, poți sări peste tot ce ține de identificator: orice operațiune fără câmpul case merge la prima casă activă. Cu două sau mai multe, trimite mereu case - altfel bonul iese pe aparatul greșit. Recomandarea rămâne să individualizezi casa indiferent de câte ai.
Câmpul case acceptă trei lucruri, căutate în ordinea asta: NUI-ul (seria fiscală), id-ul nostru, apoi numele. Folosește NUI-ul.
NUI-ul este bătut în memoria fiscală a aparatului: nu se schimbă niciodată și nu poate fi confundat cu al altei case. Id-ul este al nostru, iar numele îl scrie un om - dacă cineva redenumește sau reconfigurează două case, ele se pot inversa fără ca programul tău să afle. Cu NUI-ul nu ai cum să nimerești alt aparat.
Îl găsești în nui aici, pe bonul tipărit, și pe eticheta aparatului. (serial are aceeași valoare și rămâne în răspuns, pentru integrările scrise înainte.)
Un singur caz cere atenție: aceeași casă poate fi configurată de mai multe ori în aplicație - pe COM și pe rețea, de exemplu. Atunci NUI-ul se potrivește cu mai multe intrări, iar noi o alegem pe cea folosită cel mai recent. Dacă vrei să nu rămână nimic la voia noastră, trimite id-ul acelei configurări.
Cum se simte casa acum
Întreabă aparatul, nu evidența: răspunde dacă este conectat, ce serie are și ce semnalează (hârtie, capac, memorie fiscală).
curl http://127.0.0.1:9111/api/v1/cases/DB9999999999/status \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "online": true, "message": "Conectat",
"info": { "model": "DP-25", "serie": "DB9999999999", "cif": "RO99999999",
"flags": { "paper_end": false, "cover_open": false } } }
- Poți da
id-ul sau numele casei - merg amândouă. Ca good practice recomandăm să 'chemi' mereu casele folosind id-ul și nu denumirea alocată acesteia din secțiunea de configurare. - Casa deconectată nu este o eroare: primești
200cu"online": falseși motivul înmessage. - Durează cât durează o interogare pe aparat (unele aparate sunt mai lente sau sunt configurate cu un baud rate mai puțin generos și la toate astea adaugi viteza rețelei și a mediilor care rulează softul - nu va fi mult, dar nu va fi nici instant), de regulă sub două secunde. Nu o chema în buclă strânsă: casa servește un singur client odată.
Tipărește un bon fiscal
Operațiunea centrală. Trimiți articolele și plățile, primești numărul bonului fiscal.
curl -X POST http://127.0.0.1:9111/api/v1/receipts \
-H "Authorization: Bearer TOKENUL" \
-H "Idempotency-Key: comanda-4471" \
-H "Content-Type: application/json" \
-d '{
"case": "DB9999999999",
"receipt": {
"items": [ { "name": "APA PLATA 0.5L", "price_val": 3.50,
"qty": 2, "unit": "buc", "vat_rate": 21 }
],
"payments": [ { "method": "cash", "amount": 7.00 } ]
}
}'
{ "ok": true, "receipt_no": "958", "message": "OK" }
- Pe hârtie: antetul firmei, „APA PLATA 0.5L 2 BUC × 3.50 = 7.00 A", numărul de articole, TOTAL 7,00, NUMERAR 7,00, defalcarea TVA.
receipt_noeste numărul bonului fiscal tipărit de casă - pune-l în evidența ta.- Răspunsul vine după ce bonul a ieșit. O cerere care durează 2-3 secunde este normală.
Câmpurile unui articol
| Câmp | Obligatoriu | Ce înseamnă |
|---|---|---|
name |
da | Denumirea de pe bon. Peste 72 de caractere se taie. |
price_val |
da | Prețul unitar, cu TVA inclus, în lei. |
qty |
da | Cantitatea. Acceptă zecimale (cântar): 0.123. |
vat_rate |
da* | Cota, ca număr: 21, 11, 0. |
vat_group |
da* | Alternativ: grupa de pe casă - "A", "B", "C".
|
unit |
nu | Unitatea de măsură. Implicit buc. |
dept |
nu | Departamentul de pe casă, dacă îl folosești. |
adjust_kind |
nu | Reducere sau majorare pe articol: disc_val, disc_pct,
sur_val, sur_pct. |
adjust_value |
nu | Valoarea reducerii (lei) sau procentul. |
* Dă una din două: fie cota (vat_rate), fie grupa (vat_group). Casa tipărește întotdeauna pe grupe, iar dacă trimiți doar cota o traducem noi în grupă - după tabelul citit din aparatul tău, nu după o convenție.
Convenția obișnuită (21 → A, 11 → B, 9 → C, 5 → D, 0 → E) nu este adevărată pe orice aparat. Grupele sunt programate de tehnicianul care a fiscalizat casa, iar noi am dat peste un DP-25 cu A=21%, B=11%, C=0%, D=0% și fără grupă E. O linie cu 0% trimisă orbește pe grupa E a fost refuzată de casă la mijlocul bonului, și a ieșit un BON ANULAT pe hârtie.
De aceea aplicația citește tabelul de cote chiar din aparat (îl vezi la GET /cases/{nui}/config) și îl folosește la traducere. Dacă ceri o cotă pe care casa nu o are programată, bonul este refuzat înainte de hârtie, cu 422 și cu lista cotelor pe care casa chiar le are. Mai bine un refuz curat decât un bon anulat pe care trebuie să-l explici la control.
Dacă aparatul are două grupe cu aceeași cotă (C și D, amândouă 0%), traducerea o alege pe prima. Alegerea între ele este a magazinului: trimite vat_group explicit.
Grupa câștigă - ea pleacă la casă exact așa cum ai dat-o, fiindcă grupele sunt programate de tehnicianul care a fiscalizat aparatul și sunt lege, nu ceva de reinterpretat de noi.
Cota rămâne totuși folosită într-un loc: evidența noastră. Din ea calculăm TVA-ul pe care îl vezi în aplicație, în /totals și în listele de bonuri. Dacă trimiți doar grupa, bonul de pe hârtie este corect, dar în evidența noastră linia aceea intră cu TVA 0 - pe hârtie este adevărul fiscal, la noi ar fi o cifră greșită în tabloul de bord.
De aceea recomandarea practică este simplă: trimite amândouă, potrivite între ele. Dacă trimiți una singură și nu ești sigur cum este programată casa, trimite cota.
Plăți
| Câmp | Ce înseamnă |
|---|---|
method |
cash, card sau other. |
amount |
Suma încasată prin metoda aia. |
index |
Opțional: indicele exact al modalității din casă, dacă îl știi. Bate method. |
Verificăm asta înainte de a trimite ceva la casă. Dacă suma plăților este mai mică decât totalul, primești 400 cu diferența calculată, și pe hârtie nu iese nimic. Fără verificarea asta, casa ar tipări articolele, ar constata la final că nu se închide și ar scoate un bon anulat - un document pe care apoi trebuie să-l justifici.
Reduceri și majorări
Pe articol, cu adjust_kind și adjust_value - produsul apare la preț întreg, iar reducerea pe rândul de sub el. Pe tot bonul, cu receipt_adjust:
{ "receipt": { "items": [ … ], "receipt_adjust": { "kind": "disc_pct", "value": 10 },
"payments": [ … ] } }
- Reducerile pe articol se tipăresc sub articolul lor, care rămâne la prețul întreg. Așa se vede pe bon ce anume a fost redus, iar baza fiecărei cote de TVA scade exact acolo unde trebuie.
- Reducerea pe bon se tipărește o singură dată, ca reducere de bon, iar casa o împarte pe cotele de TVA. Noi nu o desfacem pe articole: nu știm din ce produse a compus-o programul tău, iar dacă am ghici, TVA-ul de pe hârtie ar arăta altceva decât ai vrut tu.
- Reducerea de bon se aplică pe totalul deja redus de reducerile de articol, nu pe cel brut.
Trei recomandări fiscale, pe care le urmăm și noi în programele proprii:
- Nominalizează reducerea. Când știi cărui produs îi aparține, trimite-o pe articol, nu pe bon. Diferența se vede la control: o reducere de articol spune ce s-a ieftinit, una de bon spune doar cât.
- Ai grijă ce reduci. Garanția SGR și taxele colectate pentru altcineva (ecotaxa, taxa de stațiune) nu sunt banii comerciantului: garanția se restituie integral, taxele se predau în cuantum fix. În programele Poleo le scoatem din baza reducerii. Prin API nu te oprim - trimitem ce ne dai, fiindcă răspunderea pentru conținutul bonului rămâne a programului care l-a compus - dar dacă o reducere ajunge peste ele, ajunge acolo pentru că așa ai cerut.
- Nu amesteca. Dacă ai și reduceri de articol, și una de bon, trimite-le ca atare, fiecare la locul ei. Nu le însuma într-o singură reducere de bon „ca să iasă totalul": totalul iese, dar defalcarea pe cote de pe raportul Z nu mai spune adevărul despre ce s-a vândut.
Datele firmei cumpărătoare
Când bonul se emite pe firmă - bonul fiscal cu CUI, care ține loc de factură simplificată - trimiți două câmpuri în plus, la nivelul bonului, nu al articolelor.
| Câmp | Obligatoriu | Ce înseamnă |
|---|---|---|
client_vat |
nu | CUI-ul cumpărătorului, cu sau fără prefixul RO. |
client |
nu | Denumirea firmei. Se tipărește numai împreună cu CUI-ul. |
{ "receipt": { "client": "EXEMPLU COMERT SRL", "client_vat": "RO99999999",
"items": [ … ], "payments": [ … ] } }
- Pe bon apar denumirea și CUI-ul cumpărătorului, în corpul fiscal al bonului, nu ca notă de subsol.
- Casele care au comanda de „date cumpărător" o folosesc pe aceea; celelalte le tipăresc ca text fiscal. Rezultatul pe hârtie este același, iar programul tău nu trebuie să știe care model face cum.
- Dacă o casă le refuză pe amândouă, bonul acela iese ANULAT - casa spune că nu poate abia după ce l-a deschis, iar noi nu emitem un bon pe firmă fără CUI pe el. Ținem minte refuzul pe casa aceea, așa că de la al doilea bon încolo primești
422înainte de hârtie, cu același motiv. Un singur document pierdut, o singură dată. - Plafonul de numerar se schimbă: 5.000 lei de la o persoană juridică, față de 10.000 de la o persoană fizică. Avertizarea o dăm după numărul de pe bon, nu după intenție - vezi Limite.
- Cu CUI trimis dar fără denumire, iese doar CUI-ul. Este valabil fiscal, dar dacă o ai, trimite-o: bonul se justifică singur în contabilitatea clientului.
Text stilizat și coduri de bare
Rânduri pe care le pui tu pe bon, dincolo de articole: un mesaj scos în evidență, numărul comenzii ca cod de bare, un QR care duce clientul la garanție sau la factura lui. Se cer numai prin API - formatele de fișier nu au câmpuri pentru așa ceva, fiindcă programele de vânzare nu știu să le trimită.
{ "receipt": {
"items": [ … ], "payments": [ … ],
"texts": [
"text simplu, ca până acum",
{ "text": "MULTUMIM!", "bold": true, "double_height": true, "align": "center" },
{ "barcode": "5941234567890", "type": "EAN13" },
{ "barcode": "https://magazinul-tau.ro/c/4471", "type": "QR", "size": 6 }
] } }
Text stilizat
| Câmp | Valori | Ce face |
|---|---|---|
text | până la 64 de caractere | Rândul propriu-zis. Mai lung, se taie. |
bold | true / false | Îngroșat. |
italic | true / false | Înclinat. |
underline | true / false | Subliniat. |
double_height | true / false | Înălțime dublă. |
align | left, center, right | Alinierea pe lățimea bonului. |
Coduri de bare
type | Ce acceptă barcode |
|---|---|
EAN8 | exact 8 cifre |
EAN13 | exact 13 cifre |
CODE128 | 3-31 caractere ASCII |
QR | 3-279 caractere ASCII |
I2OF5 | 3-22 cifre |
PDF417 | 3-400 caractere ASCII |
size merge de la 1 la 8 și se aplică doar la QR și PDF417 - celelalte coduri au lățimea fixată de standardul lor. Implicit 4.
- Rândurile ies în ordinea în care le-ai scris, după articole și înainte de total - în subsolul corpului bonului.
- Un cod de bare care nu respectă regula tipului lui este refuzat înainte de hârtie, cu
400și cu regula scrisă în mesaj: „Codul de bare EAN13 cere exact 13 cifre; am primit «123» (3 caractere)". Trimis mai departe, ar fi însemnat bon anulat. - Textul simplu (șir, nu obiect) merge exact ca până acum - nu trebuie să schimbi nimic la integrările existente.
- Dacă aparatul refuză un rând suplimentar, îl notăm în jurnal și bonul se termină normal. Un rând publicitar nu are voie să strice un bon fiscal.
Stilizarea și codurile de bare sunt tipărite de aparat, deci depind de el. La Datecs merg amândouă, pe comenzile din manualul producătorului. La Tremol, setul de comenzi pe care îl avem documentat nu are nici stilizare, nici coduri de bare: textul iese nestilizat, iar codul de bare se tipărește ca text, ca informația să nu se piardă - și rămâne o urmă în jurnal, ca să se vadă că modelul nu-l suportă, nu că l-am pierdut noi.
Nu inventăm comenzi pe o casă fiscală ca să pară că merge. Când primim setul de la producător, se schimbă într-un singur loc, iar tu nu ai nimic de rescris.
Tipărește un raport X sau Z
curl -X POST http://127.0.0.1:9111/api/v1/reports \
-H "Authorization: Bearer TOKENUL" \
-H "Idempotency-Key: z-2026-08-16" \
-H "Content-Type: application/json" \
-d '{"kind": "Z"}'
{ "ok": true, "receipt_no": "28", "message": "OK" }
"kind": "X"- raport de control. Nu schimbă nimic (nu "pune casa pe 0" ca un raport Z), se poate cere oricând."kind": "D"sau"DEPARTAMENTE","G"sau"GRUPE","OPERATORI","PLU"- rapoartele de nomenclator: ce este programat în casă. Nu ating ziua fiscală, nu ocupă loc în memorie și nu se pot desface, fiindcă nu schimbă nimic."kind": "Z"- închide ziua fiscală. Nu ai undo, consumi zile fiscale din memoria casei, vei avea niște justificări de dat în situația, dar fără a se limita la cea a unui control ANAF care va vedea diferențe de sertar vs raport X, în contabilitate vei trimite doua documente și lista poate continua așa că este necesar să faci cuvenita distincție între cele două metode.receipt_noeste numărul raportului Z.- Orice altceva la
kindîntoarce400, fără să atingă casa.
Pune-i o Idempotency-Key care conține ziua. Dacă programul tău reîncearcă după un timeout de rețea, cheia îl oprește să închidă ziua de două ori.
Rapoarte de nomenclator
Spun ce este programat în casă, nu ce s-a vândut azi: departamentele, grupele de articole, operatorii și articolele. Se cer pe același drum ca X și Z, doar cu alt kind.
curl -X POST http://127.0.0.1:9111/api/v1/reports \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"kind": "DEPARTAMENTE"}'
kind | Ce iese pe hârtie |
|---|---|
D / DEPARTAMENTE | Rulajul pe departamente. |
G / GRUPE | Rulajul pe grupele de articole. |
OPERATORI | Rulajul pe operatori (casieri). |
PLU / ARTICOLE | Articolele programate în casă, cu rulajele lor. |
- Nu se închide nicio zi și nu se șterge niciun registru. Sunt citiri tipărite, atât.
- Registrele operatorilor nu se golesc. Comanda casei permite și varianta „cu ștergere"; noi trimitem întotdeauna varianta care nu șterge. Golirea lor este o decizie fiscală, nu efectul secundar al unei citiri - se face de la casă, de cine și-o asumă.
- La fel, raportul de articole cu ștergere este refuzat: ar zeroa rulajele articolelor dintr-o simplă cerere de raport.
- Modelele care nu au raportul cerut prin protocol o spun limpede - „se poate scoate de la tastatura casei" - în loc să răspundă „ok" fără să iasă nimic pe hârtie.
Din 17.08.2026, o licență expirată oprește nu doar tipărirea bonurilor, ci tot ce lasă urmă în memoria fiscală: rapoartele X, Z, periodice și de nomenclator, plus depunerile și retragerile de numerar. Primești 422 cu motivul licenței.
Două operațiuni rămân dinadins în afara porții: anularea bonului rămas deschis (/void) și deschiderea sertarului (/drawer). Sunt supape: o casă blocată cu un bon deschis trebuie să poată fi deblocată chiar și cu licența expirată. Altfel n-ar fi licențiere, ar fi șantaj.
Depunere și retragere de numerar
curl -X POST http://127.0.0.1:9111/api/v1/cash \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"kind": "in", "amount": 200, "reason": "Fond de rulaj"}'
"in"= depunere,"out"= retragere. Altceva →400.- Suma trebuie să fie mai mare ca zero;
reasoneste opțional, dar ajunge în evidență și te ajută peste o lună. - Casa tipărește un document nefiscal cu operațiunea, iar soldul din sertar se schimbă.
Deschide sertarul
curl -X POST http://127.0.0.1:9111/api/v1/drawer \
-H "Authorization: Bearer TOKENUL" -d '{}'
200 și sertarul se deschide. Dacă nu este conectat niciun sertar, casa poate răspunde tot cu 200 - comanda a fost primită, doar că n-are ce deschide. Unele case nu pot gestiona sertarul constructiv, altele îl au dezactivat din soft. Deși nu oferim suport pentru setarea casei de marcat, te putem îndruma către colaboratorul nostru, QRetail având convigerea că vei găsi răspuns la orice întrebare tehnică legată -și- de aparatele fiscale.
Copia ultimului bon
Iese o copie a ultimului bon, marcată ca atare de casă. Nu este un bon fiscal nou și nu afectează totalurile zilei. Unele modele permit o singură copie per bon.
Anulează bonul deschis
Anulează bonul rămas deschis pe casă - situația în care o vânzare a început și nu s-a încheiat. Iese pe hârtie un bon marcat ANULAT, care se păstrează pentru evidență. Dacă nu este niciun bon deschis, primești 422 cu explicația casei.
/void se referă doar la bonul început și neterminat. Un bon fiscal închis nu se mai poate anula - nici de noi, nici de casă, nici de producătorul ei. Odată tipărit, el a intrat în memoria fiscală și în jurnalul electronic, iar acolo nu se șterge nimic. Un API care ți-ar promite altceva ți-ar promite o ilegalitate.
Ce se face, când s-a emis un bon greșit, este o declarație de bon greșit întocmit: un document prin care comerciantul explică eroarea, iar suma se scoate din încasările zilei. Se poate cere și prin API. Dacă marfa se dă înapoi și clientul își primește banii, atunci nu este o declarație, ci un retur - două lucruri diferite, cu documente diferite.
Bon fiscal emis greșit - ce se poate face
Pe scurt: bonul rămâne. Se întocmește o declarație, se corectează încasarea, iar hârtia greșită se atașează la ea.
Procedura din aplicație
- Se citește avertismentul și se bifează asumarea. Scrie negru pe alb ce urmează: bonul rămâne, documentul este o declarație pe proprie răspundere, iar el o semnează. Bifa există ca operațiunea să nu se facă din obișnuință.
- Se alege bonul și motivul. Bonul se ia din istoricul aplicației, cu numărul lui fiscal și suma reală - nu se scrie de mână.
- Se întocmește declarația. Numerotată continuu, în format PDF, cu bonul, suma, motivul și gestionarul. Rămâne în registrul din aplicație.
- Se corectează numerarul. Dacă banii au fost încasați în numerar, aplicația propune și retragerea corespunzătoare de pe casă, ca soldul sertarului să spună adevărul.
- Se anulează și în programul de vânzări. Noi nu putem face asta în locul tău - stocul și evidența comercială sunt la tine.
Prima versiune cerea o parolă și un cod trimis administratorului. S-a renunțat: documentul este o declarație pe proprie răspundere, semnată de gestionar - nu ceva ce aprobă altcineva. În locul validării au rămas lucruri care contează mai mult: avertismentul de bifat, motivul scris, documentul semnat, și un email către contabilitate a doua zi dimineață, cu tot ce s-a anulat și s-a returnat în ziua trecută. A doua zi, nu seara, ca să prindă și magazinele care lucrează până la miezul nopții.
- Declarația se poate cere prin API:
POST /declarations. Documentul iese la fel, iar gestionarul îl semnează la fel. - Ce poți face prin API: citești registrul la
GET /declarationsși descarci PDF-ul fiecărei declarații. Suficient ca să ții evidența ta la zi și să atașezi documentul unde trebuie. - Dacă greșeala se prinde înainte de închiderea bonului, nu ai nevoie de nimic din toate astea:
POST /void, și vânzarea se reia curat.
Întocmește declarația de bon greșit
Scoate PDF-ul declarației pentru un bon din evidență. Nu anulează bonul - bonul fiscal rămâne în memoria fiscală și în jurnalul electronic.
curl -X POST http://127.0.0.1:9111/api/v1/declarations \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"receipt": "01M0…", "reasons": ["cantitate"], "operator": "Ana Pop",
"cash_out": true, "acknowledged": true}'
acknowledged: trueeste obligatoriu. Nu este o formalitate: înseamnă că un om a văzut ce se întâmplă și își asumă documentul pe care îl va semna. Fără el primești400.receipteste id-ul bonului din evidență (îl ai din /receipts), nu numărul fiscal.reasonsse ia dinGET /api/v1/declaration-reasons- trimiți cheile, nu textele. Cel puțin una.cash_out: trueînseamnă că banii se scot din sertar (retragere pe casă). Trimite-l doar dacă banii chiar se dau înapoi.- Răspunsul conține numărul din registru și calea PDF-ului. Documentul se tipărește și se semnează; se păstrează la casierie împreună cu bonul greșit.
Retur pe un bon emis
Clientul aduce marfa înapoi și își primește banii. Restituie suma (retragere de numerar din sertar sau virare pe card), marchează în evidență ce s-a returnat și întocmește procesul-verbal de restituire, în PDF.
În alte sisteme ai fi trimis un bon cu cantități negative. Aici nu: casele de marcat din România refuză documentul la mijloc și scot o hârtie marcată BON ANULAT. Am probat-o pe aparat, exact ca să nu ajungi tu să o probezi în magazin. Un bon cu cantitate negativă trimis la /receipts este refuzat înainte să se atingă hârtia, cu motivul scris.
Bonul fiscal inițial rămâne valabil. Ce se schimbă sunt banii și hârtia de justificare - nu memoria fiscală.
curl -X POST http://127.0.0.1:9111/api/v1/returns \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"receipt": "01M0…", "reason": "produs defect",
"requested_by": "Ana Pop", "acknowledged": true}'
curl -X POST http://127.0.0.1:9111/api/v1/returns \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"receipt": "01M0…", "reason": "produs defect",
"requested_by": "Ana Pop", "acknowledged": true,
"items": [{"line_no": 1, "name": "VIN", "qty": 1, "amount": 40.00}]}'
{ "ok": true,
"message": "Retur de 40.00 lei restituit din sertar.",
"document": "…\declaratii\retur_01M0….pdf",
"return": { "id": "01M0…", "status": "done", "amount": 40.00,
"method": "cash", "fiscal_no": "0123" } }
acknowledged: trueșireasonsunt obligatorii. Un retur scoate bani din sertar fără să iasă marfă pe ușă - este cea mai la îndemână metodă de furt mărunt, deci nu se face din reflex și nu se face fără motiv scris.line_noeste poziția liniei în bon, de la 0 - aceeași ordine din GET /receipts/{id}.- Nu se poate returna de două ori același lucru: se ține socoteala pe bon și pe linie. Ce s-a returnat deja este scăzut, iar peste rest primești
400cu suma rămasă. - Numerar: se face o retragere pe casă, cu explicația legată de bonul inițial. Sertarul și evidența rămân corecte.
- Card: se încearcă virarea prin terminal. Dacă terminalul nu știe operațiunea, returul rămâne
approved(„de reglat") - restituirea se face pe altă cale și contabilitatea o reglează. Nu inventăm nimic. - Află dinainte dacă terminalul poate vira înapoi:
GET /terminalsîți dă, pentru fiecare terminal,can: {refund, void, settlement}. Citește-l înainte de a-i spune clientului că își primește banii pe card - altfel afli abia după ce a predat marfa. Astăzi, dintre terminalele pe care le știm, niciunul nu are retur; nu este o lipsă a noastră, este ce oferă driverele și contractele. - Dacă restituirea eșuează (casa refuză retragerea), returul rămâne
failed, fără document, și primești422. Un proces-verbal de restituire pentru bani care nu au plecat ar fi o hârtie mincinoasă. - Documentul se semnează de gestionar și de client. Ca să iasă complet, are nevoie de datele firmei - vezi /company.
- Tot ce s-a returnat într-o zi pleacă a doua zi dimineață, într-un email către contabilitate.
Retururile
Lista retururilor, cu starea fiecăruia. Un retur anume, cu liniile lui, la GET /api/v1/returns/{id}.
curl http://127.0.0.1:9111/api/v1/returns \
-H "Authorization: Bearer TOKENUL"
done- banii s-au dat înapoi, documentul există.approved- „de reglat": terminalul nu a putut vira pe card, restituirea se face pe altă cale.failed- nu s-a întâmplat nimic; aparatul a refuzat. Se reia.rejected- respins.
Setările stației
Citești cum e configurată stația și schimbi o parte din setări - cele care țin de cum arată și cum se poartă bonul, de praguri și de identitatea stației.
Se pot schimba 69 din cele 73 de setări ale aplicației, inclusiv preluarea de pe SFTP. Rămân blocate patru: cele ale serviciului API însuși - pornit, ascultă în rețea, port, adrese permise din browser.
Motivul nu e prudență generală, ci că alea patru sunt chiar ușa. Un serviciu care își poate schimba portul, se poate expune în rețea sau își poate rescrie cheia și-ar administra singur accesul, iar omul din magazin ar afla ultimul. Ele se pornesc de om, în aplicație - înainte ca API-ul să fie autorizat.
Restul se scriu, inclusiv cele care par grele: raportul Z automat, adresele de email, terminalul de card, backupul. Cine are jetonul poate deja tipări bonuri fiscale - operațiunea cea mai greu de desfăcut din toate. Comparativ, a porni aplicația sau a schimba o adresă e mai puțin grav; și tocmai de aceea accesul la API nu e liber, ci autorizat.
Parolele se scriu, dar nu se citesc niciodată înapoi. Poți configura contul de email al clientului; nu poți scoate parola lui dintr-un jeton furat. La citire primești value: null, secret: true și completata, atât cât să știi dacă mai e nevoie s-o pui.
Aproape toate intră în vigoare pe loc - bifele de bon se citesc la fiecare bon, ceasul de rapoarte și oglinda bazei la fiecare tur, poșta la fiecare mesaj. Singura cu efect în afara bazei e pornirea cu Windows: acolo rescriem chiar sarcina din sistem, în aceeași cerere.
curl http://127.0.0.1:9111/api/v1/settings -H "Authorization: Bearer TOKENUL"
{ "ok": true, "writable": 69, "locked": 4,
"settings": [
{ "key": "bon.sertar_mereu", "type": "bifa", "value": false,
"label": "Deschide sertarul la fiecare operațiune", "writable": true },
{ "key": "num.plafon", "type": "numar", "value": "45000", "writable": true },
{ "key": "rap.auto_z", "value": "0", "writable": false,
"reason": "Raportul Z închide ziua fiscală. Nimic automat cu implicații fiscale…" } ] }
curl -X POST http://127.0.0.1:9111/api/v1/settings -H "Authorization: Bearer TOKENUL" -H "Content-Type: application/json" -d '{"settings": {"bon.sertar_mereu": true, "num.plafon": 40000,
"bon.qr_mode": "anaf"}}'
{ "ok": true,
"changed": { "bon.sertar_mereu": true },
"rejected": { "api.port": "Portul serviciului nu se schimbă din serviciu…" },
"message": "o setare schimbată, una refuzată." }
- Se aplică tot ce e bun și se raportează tot ce nu e - nu ne oprim la prima greșeală. Altfel, un program care trimite zece setări și greșește una nu ar mai ști care dintre celelalte nouă au intrat.
200doar dacă a intrat ceva. Dacă nimic nu a trecut, primești400cu aceleași două liste îndetails- un200gol ar trece nevăzut prin codul tău.- Tipurile:
bifa(acceptătrue/false,1/0,"on"),numar(cu interval),lista(cuoptionsîn răspuns),text(cu lungime maximă). - Cheile blocate răspund cu
reason. Sunt doar cele patru ale serviciului API; se schimbă din fereastra aplicației, de un om. - SFTP-ul se poate porni și configura de aici (
sftp.*): e al doilea drum prin care intră bonuri în casă - fișierele coboară în folderul monitorizat și trec prin aceleași verificări ca cele locale. Parola se scrie, dar nu se citește înapoi, și se păstrează cifrată pe stație; mai bine foloseștesftp.key, calea către cheia privată, și atunci nu mai stă nicio parolă acolo. - Tipul
oravrea"HH:MM"și se normalizează („8:30" devine „08:30"). - Raportul Z automat (
rap.auto_z) se poate porni de aici. Ține minte ce înseamnă: închide ziua fiscală la ora stabilită, iar o zi închisă nu se poate desface. - Fereastra de Setări deschisă în acel moment nu se reîmprospătează singură; valoarea e deja schimbată, dar se vede la următoarea deschidere a paginii.
Datele firmei
Denumirea, forma juridică, sediul, Registrul Comerțului, CUI-ul și capitalul social - mențiunile pe care Legea 31/1990 (art. 74) le cere pe documentele societății. Fără ele, procesul-verbal de retur și declarația de bon greșit ies incomplete, cu lipsa scrisă pe ele.
curl -X POST http://127.0.0.1:9111/api/v1/company \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"nume": "EXEMPLU COMERT S.R.L.", "forma": "S.R.L.",
"adresa": "Str. Exemplului nr. 10, Craiova", "reg_com": "J16/9999/2019",
"cui": "RO99999999", "capital": "200 lei"}'
GETîți dă și lista câmpurilor acceptate (fields) și pe cele care lipsesc (missing).- Trimiți doar câmpurile pe care le schimbi; restul rămân cum erau.
- Cu
case(NUI-ul casei) scrii datele doar pentru casa aceea. Același calculator poate deservi case ale unor firme diferite, iar un document ieșit cu firma greșită este o problemă reală, nu una de interfață.
Retur pe card
Trimite banii înapoi la client, pe cardul lui. Marfă returnată, serviciu neprestat, încasare făcută greșit.
curl -X POST http://127.0.0.1:9111/api/v1/pos/refund \
-H "Authorization: Bearer TOKENUL" \
-H "Idempotency-Key: retur-4471" \
-H "Content-Type: application/json" \
-d '{"amount": 149.90}'
- Returul scoate bani reali din contul comerciantului. Nu-l porni dintr-un automatism - pune o decizie de om în fața lui.
- Nu este totuna cu anularea. Returul merge oricând, inclusiv după închiderea borderoului; anularea merge doar cât timp plata este în borderoul curent.
- Pune-i
Idempotency-Key. Un retur trimis de două ori dintr-un timeout de rețea înseamnă bani dați de două ori. - Returul pe card nu emite niciun bon pe casă - și nici nu ai cum să ceri unul: bonul de storno (cantități negative) nu există în Poleo Fiscal, fiindcă nu există nici pe casele de marcat din România. Restituirea pe un bon deja emis se face cu /returns, care scoate banii și întocmește procesul-verbal.
Anulează o plată cu cardul
Scoate o plată din borderoul curent al terminalului, ca și cum nu ar fi existat.
curl -X POST http://127.0.0.1:9111/api/v1/pos/void \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"stan": "000123", "amount": 149.90}'
staneste numărul tranzacției din terminal, primit în răspunsul plății. Păstrează-l la fiecare încasare - fără el nu mai ai cu ce anula.- Se cere și suma exactă. Terminalele o cer tocmai ca să nu anulezi altă tranzacție decât cea la care te gândeai.
- După închiderea de zi a terminalului, anularea nu mai este posibilă - de acolo încolo se face retur.
Închiderea de zi a terminalului
Trimite borderoul la bancă și pornește decontarea.
curl -X POST http://127.0.0.1:9111/api/v1/pos/settlement \
-H "Authorization: Bearer TOKENUL" \
-H "Idempotency-Key: settlement-2026-08-17"
- Nu are nicio legătură cu raportul Z al casei de marcat. Sunt două zile diferite: una a terminalului bancar, alta a aparatului fiscal. Poți închide una fără cealaltă.
- Nu o facem noi automat. Are efect asupra decontării banilor, deci o cere omul - aceeași regulă ca la Z.
- Cheia de operațiune care conține ziua o face repetabilă în siguranță.
Când aparatul nu are operațiunea
Nu orice terminal are retur, anulare sau închidere de zi, și nu orice casă de marcat are tot ce știe protocolul. Un „nu se poate" de felul ăsta este definitiv: nu se schimbă nici peste cinci minute, nici mâine.
{ "ok": false,
"error": { "code": "unsupported",
"message": "Returul pe card nu este suportat de banca_x T100",
"details": { "bank": "banca_x", "model": "T100" } } }
422înseamnă „acum nu se poate" - hârtie terminată, casă ocupată, zi fiscală deschisă. Astea trec, deci merită reîncercate.501înseamnă „aparatul ăsta nu are operațiunea". Nu reîncerca niciodată. Arată omului mesajul: soluția este alt terminal, alt model, sau alt contract cu banca - nu o buclă de retry.detailsspune exact ce aparat a refuzat, ca să știi ce înlocuiești.- Ca să nu ajungi aici prin surprindere, cere /methods la pornire și vezi ce știe stația.
Export pentru ANAF
curl -X POST http://127.0.0.1:9111/api/v1/anaf \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"from": "01-08-2026", "to": "31-08-2026"}'
- Fișierul se scrie în folderul de export al casei;
messagespune unde. - Poți da
out_dirca să alegi tu folderul. - Casa citește din memoria fiscală - pe intervale mari poate dura zeci de secunde (chiar minute întregi dacă acel punct de vânzare are multe tranzacții -chiar valori mici, dar flux ridicat-). Pune un timeout generos în clientul tău.
Istoricul bonurilor
Din evidența aplicației, nu de pe casă - deci nu blochează aparatul în mijlocul unei vânzări.
curl "http://127.0.0.1:9111/api/v1/receipts?limit=3&from=2026-08-16" \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "limit": 3, "offset": 0,
"receipts": [
{ "id": "01M0…", "fiscal_no": "1006", "status": "ok",
"total": 55.0, "ts": "2026-08-16 18:19:02" } ] }
Filtre
| Parametru | Ce face |
|---|---|
case |
Doar bonurile unei case. |
from, to |
Interval de date calendaristice. |
status |
ok, error, pending, dead. |
search |
Caută în denumiri și în numele fișierului sursă. |
limit, offset |
Paginare. limit maxim 500. |
Bonurile vin de la cel mai nou spre cel mai vechi. status: "pending" înseamnă că bonul a fost primit, dar încă n-a ieșit - casa era ocupată sau deconectată; se tipărește singur când revine.
Un bon anume, cu tot conținutul
Pe lângă antet (număr fiscal, total, TVA, oră) primești și content - liniile exact cum au plecat spre casă. E util când vrei să lămurești, peste o săptămână, ce anume s-a trimis, nu ce credeai că se trimite.
Rapoartele tipărite
curl "http://127.0.0.1:9111/api/v1/reports?kind=Z&limit=5" \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "reports": [ { "kind": "Z", "report_no": 28, "total": 115683.06,
"ts": "2026-08-16 17:18:45" } ] }
Aici sunt rapoartele deja tipărite, cu totalurile lor. Ca să tipărești unul nou, folosești POST /reports. Filtrele sunt aceleași ca la bonuri, plus kind=X sau kind=Z. Nu uita ce am zis despre sancțiunea emiterii greșite a rapoartelor Z.
Totalurile zilei
{ "ok": true,
"today": { "bonuri": 99, "total": 229938.75, "tva": 39824.04, "avg": 2322.61,
"z_count": 3, "last_z": "17:18" } }
Cifrele sunt din evidența noastră, adunate din bonurile ieșite azi - bune pentru un tablou de bord. Pentru cifra oficială a zilei, raportul X al casei rămâne sursa (sau Z dacă totul este confirmat și situația permite tipărirea acestuia).
Erorile și avertizările
Exact ce vede operatorul în banda de sus a aplicației. Util dacă vrei să afli din sistemul tău când sau dacă un magazin are o problemă, fără să sune cineva.
{ "ok": true,
"active": [ { "kind": "paper_end", "severity": "warn",
"message": "Nu mai este hârtie în casa de marcat…",
"opened_at": "2026-08-16 18:02:11" } ],
"recent": [ … ] }
active= ce nu s-a rezolvat încă.recent= și cele închise.severity:warn(in aplicație este un mesaj pe fond galben, se poate lucra) saualert(in aplicație este un mesaj pe fond roșu, ceva a "crăpat").kindeste stabil și se poate verifica în cod:paper_end,cover_open,fm_full,queue,op_error…
Jurnalul electronic - salvările
Registrul salvărilor de jurnal: când s-a făcut fiecare, pe ce casă, pe ce interval, câte fișiere a adus și unde le-a pus.
curl "http://127.0.0.1:9111/api/v1/je?limit=3" \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "exports": [
{ "id": "01M04VA5GX7SY883TVE3SR2DRS",
"case": "01M04R5GFS7N3BAMP7JBMWTAZ4",
"ok": true, "files": 23,
"from": "2026-07-17", "to": "2026-08-16",
"auto": true,
"dir": "D:\\Poleo Fiscal\\je_backup\\DB9999999999",
"ts": "2026-08-16 11:34:17" } ] }
- O salvare nu este un fișier, ci o vizită la casă: aducem fișierele zilelor fiscale care lipseau din folderul casei.
filesspune câte a adus atunci. ok: falseînseamnă că vizita a eșuat, iarmessagespune de ce (casa era ocupată, ziua nu avea niciun Z, aparatul este la alt calculator).- Fișierele se cer separat, pe id - vezi mai jos.
Jurnalul electronic - ce fișiere există
Salvarea, plus lista fișierelor din folderul casei ei: nume, mărime, data ultimei scrieri.
curl http://127.0.0.1:9111/api/v1/je/01M04VA5GX7SY883TVE3SR2DRS \
-H "Authorization: Bearer TOKENUL"
{ "ok": true,
"export": { "id": "01M04VA5GX…", "files": 23, … },
"files": [
{ "name": "2026-08-16_1234567890_Z0026.p7b",
"size": 2084, "modified": "2026-08-16 11:34:17" } ] }
- Lista este a folderului casei, nu a salvării: fișierele nu se pot împărți pe vizite, fiindcă aceeași zi fiscală poate fi adusă de mai multe salvări. Numele conține data, CUI-ul și numărul raportului Z, deci se citește ușor cu ochiul.
- Fișierele
.p7bsunt semnate de casa de marcat. Noi doar le descărcăm și le păstrăm; nu le atingem, nu le semnăm și nu le putem reface dacă lipsesc. - Dacă acea casă nu are încă folder (nicio salvare reușită), primești
404cunot_found.
Jurnalul electronic - descarcă un fișier
Fișierul însuși, exact așa cum l-a semnat casa. Aici nu primești JSON, ci documentul.
curl -o Z0026.p7b \
http://127.0.0.1:9111/api/v1/je/01M04VA5GX…/files/2026-08-16_1234567890_Z0026.p7b \
-H "Authorization: Bearer TOKENUL"
HTTP/1.1 200 OK
Content-Type: application/pkcs7-mime
Content-Disposition: attachment; filename="2026-08-16_1234567890_Z0026.p7b"
Content-Length: 2084
<conținutul binar al fișierului semnat>
- Conținutul vine în flux, oricât ar fi de mare - nu îl ținem în memorie și nu există prag peste care să refuzăm.
- Numele fișierului vine în
Content-Disposition, deci un browser sau un client HTTP îl salvează cu numele corect, fără să-l ghicești. - Dacă programul tău preferă JSON, adaugă
?as=base64: primești{ "name", "size", "encoding": "base64", "content" }. Aici există un prag - 8 MB - fiindcă base64 se construiește întreg în memorie. Peste el, cere fișierul normal. - Numele se ia din listă, nu se compune. Orice încercare de a ieși din folderul casei (
../, cale absolută) este refuzată.
Descarcă des, nu la nevoie. Memoria unei case ține un istoric limitat - la Datecs, în jur de două luni în afară de cea curentă, iar când se umple, cea mai veche perioadă iese ca să încapă cea nouă. Ce nu ai descărcat până atunci nu se mai poate lua din API: recuperarea se face doar de un tehnician autorizat, iar dacă nu ai unul, te putem îndruma către colaboratorul nostru QRetail.
Aplicația face oricum o salvare automată după fiecare raport Z (se poate opri din Setări → Backup JE), tocmai ca să nu depinzi de memoria casei.
Declarațiile de bon greșit
Registrul declarațiilor întocmite în aplicație pentru bonurile emise greșit, cu PDF-ul fiecăreia.
curl http://127.0.0.1:9111/api/v1/declarations \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "declarations": [
{ "id": "01M0…", "seq_no": 7, "receipt_no": "958",
"amount": 7.00, "reason": "cantitate greșită",
"operator": "Ana", "cash_out": true,
"pdf": true, "file": "declaratie_7_958.pdf",
"ts": "2026-08-16 12:04:11" } ] }
seq_noeste numărul din registru - același care apare pe PDF. Numerotarea este continuă și nu se reia.cash_outspune dacă s-a făcut și retragerea de numerar corespunzătoare pe casă.- PDF-ul se descarcă la
/api/v1/declarations/{id}/file, la fel ca fișierele de jurnal (acelașiContent-Disposition, același?as=base64).
Tot ce știm despre o casă
Ce este configurat la noi și ce este programat în aparat: cotele de TVA pe grupe și sloturile de încasare, citite chiar de pe casă.
curl http://127.0.0.1:9111/api/v1/cases/DB9999999999/config \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "case": {
"nui": "DB9999999999", "name": "DB9999999999",
"brand": "datecs", "model": "DP-25", "transport": "tcp",
"operator": "1", "till": "1", "department": "1",
"pay_dialect": "casa", "pay_map": { "card": "1" },
"device": {
"read_ok": true,
"vat": [ { "group": "A", "rate": 21 }, { "group": "B", "rate": 11 } ],
"payments": [ { "index": 0, "name": "NUMERAR" },
{ "index": 1, "name": "CARD" } ] } } }
device.vatsunt cotele programate în casă, pe grupe. Ele sunt adevărul după care tipărește ea - dacă vrei să trimițivat_group, de aici îl iei, nu din presupuneri.device.paymentssunt sloturile de încasare, cu numele pe care li l-a dat tehnicianul la fiscalizare. De aici afli pe ce index stă cardul la aparatul ăsta.pay_dialectspune cum se citesc numerele pe care le trimiți tu înindex:casa= chiar sloturile de mai sus;standard= convenția clasică a integratorilor.- Citirea interoghează aparatul, deci durează cât o comandă pe casă și o ține ocupată. Nu o chema într-o buclă de tablou de bord - cere-o o dată, la pornirea integrării, și ține minte răspunsul.
- Dacă aparatul nu răspunde, primești tot
200, cudevice.read_ok: falseși motivul îndevice.message- restul configurării, cea de la noi, este oricum acolo.
Licența
Câte zile mai are fiecare casă, în ce stare este și cât mai poate lucra fără internet.
curl http://127.0.0.1:9111/api/v1/license \
-H "Authorization: Bearer TOKENUL"
{ "ok": true, "licenses": [
{ "nui": "DB9999999999", "name": "DB9999999999",
"ok": true, "status": "trial", "trial": true,
"days_left": 24, "bons_left": 65,
"offline": false, "offline_days_left": 5,
"message": "Perioada de probă este activă (mai sunt 23 zile)." } ] }
- Licența se dă pe NUI, nu pe calculator: o stație cu două case are două licențe, care pot expira în zile diferite.
status:trial,active,expired,blockedsauerror. Peok: falsecasa nu mai tipărește - este singura stare care oprește vânzarea.- În probă,
bons_leftspune câte bonuri mai are. Nu întrebăm serverul la fiecare bon: numărătoarea este locală, iar verificarea online se face rar. offline_days_left= de câte zile mai poate lucra fără internet. Dacă estenull, nu este o licență cu termen offline.- Bun de pus într-un tablou de bord: cine ține 30 de magazine află de aici că unul expiră săptămâna viitoare, în loc să afle din telefonul casierului în dimineața în care casa nu mai tipărește.
Terminalele de card
Terminalele bancare configurate, cu id-urile lor. De aici iei valoarea pentru terminal la plată.
{ "ok": true, "enabled": true, "routing": "dupa_casa",
"terminals": [ { "id": "01KZVBQPJFWSHHTKC8QFQ5YAKT",
"name": "Terminal casa 1", "model": "Newland N910",
"case": "01M04R5GFS…", "transport": "tcp",
"can": { "refund": false, "void": true, "settlement": true } } ] }
routingspune ce se întâmplă dacă nu alegi terminalul:dupa_casa= cel de lângă casa care tipărește,fix= mereu același, ales de comerciant,intreb= îl alege operatorul pe ecran.enabled: falseînseamnă că plata cu cardul nu este activată în aplicație - orice cerere la/posva fi refuzată până se bifează în Setări.canspune ce operațiuni are terminalul:refund(retur),void(anularea din borderoul curent),settlement(închiderea de zi). Citește-l înainte de a promite ceva clientului. Nu costă nimic - se citește din driver, nu de pe fir - și te scutește de situația în care afli că nu se poate abia după ce clientul a predat marfa.- Ce nu apare aici nu se reîncearcă: dacă
refund: false, operațiunea va răspunde501cuerror: "unsupported"oricând ai încerca. Soluția este alt terminal sau alt contract cu banca, nu o reîncercare.
Plătește cu cardul
Trimite suma la terminalul bancar și așteaptă răspunsul băncii. Separat de bon, și dinadins.
Poleo Fiscal nu vede, nu prelucrează și nu stochează date de card: nici numărul cardului (PAN), nici data expirării, nici CVV/CVC, nici PIN-ul. Nu are cum - nu ajung la el.
Ce se întâmplă de fapt: programul trimite terminalului o sumă, prin protocolul ECR al producătorului sau al băncii, și primește înapoi rezultatul plus elementele de reconciliere - cod de autorizare, RRN, STAN și, când terminalul le dă, ultimele cifre ale cardului. Cardul e citit, criptat și autorizat integral de terminal, care ține de bancă.
Prin urmare, nici programul și nici API-ul lui nu se află în fluxul datelor de card. Elementele de reconciliere rămân în evidența locală a comerciantului, ca să poată potrivi încasările cu extrasul bancar; le citești cu GET /card-payments.
curl -X POST http://127.0.0.1:9111/api/v1/pos \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"amount": 149.90, "terminal": "01KZVBQPJFWSHHTKC8QFQ5YAKT"}'
{ "ok": true, "message": "Aprobat",
"payment": { "approved": true, "auth": "004512",
"rrn": "622812345678", "card": "****4471", "terminal": "Terminal casa 1" } }
- Întâi banii, apoi bonul. Ceri plata, și doar dacă banca aprobă tipărești bonul cu
"method": "card". Invers, ai un bon fiscal emis pentru o plată refuzată - și el nu se mai retrage. - Fără
terminal, plata merge unde spunerouting. Cu el, merge exact acolo - util când vrei încasarea pe banca cu comisionul mai mic, sau pe terminalul casieriei de la care s-a făcut vânzarea. - Păstrează
authșirrn: cu ele reconciliezi cu extrasul băncii și cu ele se lămurește o dispută. - Plata incertă este cazul care contează: terminalul a primit comanda, dar răspunsul nu a ajuns înapoi. Cardul poate să fi fost debitat. Nu relua plata automat - se verifică pe terminal întâi. Aplicația ține evidența acestor plăți și le semnalează separat.
Cum leg un site la casa de marcat
Întrebarea vine des, așa că răspunsul pe scurt, înainte de pași: site-ul nu vorbește direct cu casa. Casa este un aparat din magazin, legat prin cablu, rețea locală sau bluetooth la calculatorul pe care rulează Poleo Fiscal. Serverul site-ului tău este în altă parte, pe internet.
- Poleo Fiscal rulează pe calculatorul din magazin, lângă casă, cu API-ul pornit din Setări → API.
- Serverul tău trimite comanda către acel calculator, nu către casă. Dacă este în aceeași rețea, bifezi „Ascultă în rețea" și îi dai adresa locală. Dacă este pe internet, ai nevoie de o cale până acolo - VPN sau un tunel; nu deschide portul direct în internet.
- Bonul pleacă din backend, nu din browser. Tokenul stă pe serverul tău. Pus în JavaScriptul unei pagini publice, oricine îl citește și îți tipărește bonuri pe aparat.
- Fiecare comandă primește o cheie de idempotență - numărul comenzii din magazinul tău online este alegerea firească. Dacă rețeaua pică între „a tipărit" și „am primit răspunsul", reîncercarea nu scoate al doilea bon.
- Numărul bonului fiscal se salvează la tine, pe comandă. E dovada că vânzarea aia a fost fiscalizată, și tot el leagă comanda din site de raportul Z al zilei.
Pașii ăștia sunt pentru magazinul online: clientul comandă de acasă, deci nimeni din magazin nu are pagina deschisă. Dacă în schimb pagina ta e ținută deschisă de casierul de la tejghea, atunci browserul lui e deja în rețeaua casei și poate chema API-ul direct - vezi Clienți. Regula care nu se schimbă în niciun caz: portul API nu se deschide direct în internet.
POST http://192.168.0.50:9111/api/v1/receipts
Authorization: Bearer TOKENUL
Idempotency-Key: comanda-2026-4471
Content-Type: application/json
{ "case": "DB9999999999",
"receipt": {
"items": [ { "name": "ABONAMENT LUNAR", "price_val": 149.90,
"qty": 1, "vat_rate": 21 } ],
"payments": [ { "method": "card", "amount": 149.90 } ] } }
- Bonul iese pe casa cu NUI-ul
DB9999999999, iar tu primești numărul lui fiscal înreceipt_no. - Plata a fost încasată deja de procesatorul site-ului, deci pe bon o declari
card- nu mai treci prin/pos, care este pentru terminalul fizic din magazin. - Dacă în clipa aceea casa este oprită sau ocupată, bonul nu se pierde: intră în coadă și iese singur când aparatul revine. Îl vezi cu
status: "pending"înGET /receipts. - Dacă vinzi doar online, întreabă-ți contabilul dacă ai nevoie de casă de marcat - pentru încasările cu cardul, la distanță, de multe ori nu este obligatorie. API-ul ăsta rezolvă partea tehnică, nu pe cea de obligație legală.
Cartea de vizită a stației
Ce treci în sistemul tău ca să înregistrezi stația: amprenta ei, locația și casele pe care le poate comanda.
{ "ok": true, "station": {
"fingerprint": "PF-d1e637c92ea2d1fe4eeaf41708a7ad22",
"location": "Magazin Centru", "till": "1",
"app": "poleo-fiscal", "api": "v1",
"cases": [ { "nui": "DB9999999999", "name": "DB9999999999",
"brand": "datecs", "model": "DP-25" } ] } }
- Amprenta nu are legătură cu licența și nu se schimbă singură - nici la actualizare, nici dacă se schimbă portul, IP-ul sau casa. O înregistrezi o dată.
- Se poate roti, dar numai dacă omul din magazin o cere din Setări → API. Atunci legăturile trebuie aprobate din nou - este parolă de aplicație, nu identitate.
locationeste numele din Setări → „Denumirea locului de muncă". După el rutezi comenzile: casele se schimbă, locul rămâne.
Ce știe versiunea asta
Lista metodelor, ca să afli din cod ce poate stația - fără să ghicești după manual ce s-a mai adăugat.
{ "ok": true, "api": "v1", "methods": [
{ "method": "POST", "path": "/api/v1/receipts",
"description": "Tipărește un bon fiscal" }, … ] }
Aceeași listă o vede și omul în aplicație, în Setări → API - amândouă vin din același loc din program, deci nu se pot despărți una de alta.
Restul citirilor din evidență
Toate răspund din evidența aplicației, deci nu ating aparatul și nu-l țin ocupat. Acceptă aceleași filtre ca listele de bonuri: case, from, to, limit, offset.
| Calea | Ce dă |
|---|---|
GET /cash |
Mișcările de numerar: depuneri, retrageri, marcajele „depus la bancă". Filtru kind=in|out|bank. |
GET /drawer |
Soldul sertarului acum, plafonul setat și dacă este depășit. Cifra după care se decide depunerea la bancă. |
GET /vat |
Defalcarea pe cote a zilei, pe grupele A-G. /totals dă doar totalul. |
GET /card-payments |
Plățile cu cardul, pentru reconciliere cu extrasul băncii. Cu ?uncertain=true, doar cele nelămurite. |
GET /anaf |
Starea raportării, întrebat aparatul: câte Z-uri n-au ajuns, dacă este blocat la 24h. Ce nu știe modelul vine null, nu zero. |
Din /card-payments, uncertain: true înseamnă că terminalul a primit comanda dar răspunsul nu a ajuns înapoi - cardul poate să fi fost debitat. Nu relua plata din program: se verifică întâi pe terminal. E singurul caz în care nu știm, și preferăm să-ți spunem asta decât să-ți dăm un „nu" liniștitor.
Raport periodic din memoria fiscală
Același drum ca X și Z, cu kind: "PERIODIC" și un interval.
curl -X POST http://127.0.0.1:9111/api/v1/reports \
-H "Authorization: Bearer TOKENUL" \
-H "Content-Type: application/json" \
-d '{"kind": "PERIODIC", "from": "01-08-2026", "to": "31-08-2026"}'
- Iese pe hârtie raportul pe intervalul cerut, citit din memoria fiscală a casei.
detailed: falsecere varianta sumară, unde modelul o are.- Pe intervale mari poate dura zeci de secunde: casa citește înregistrare cu înregistrare. Pune un timeout generos.
- Nu schimbă nimic și nu închide ziua - este o citire, nu un Z.
Când programul tău este în altă rețea
Până aici, tot manualul a presupus că programul care comandă casa ajunge la calculatorul din magazin. Dacă ai un site, sau un ERP în cloud, nu ajunge - și merită spus pe șleau, fiindcă aici se fac cele mai multe greșeli de securitate.
Nu îți cerem, și nu îți recomandăm, să deschizi portul API în internet sau să faci port-forward pe routerul magazinului. O casă fiscală ajunsă publică înseamnă că oricine îi ghicește tokenul îți tipărește bonuri - iar bonurile tipărite nu se retrag.
Stația inițiază întotdeauna legătura. Nimic nu intră spre ea neinvitat.
Puntea este a ta, nu a noastră
Poleo Fiscal nu găzduiește un serviciu prin care să treacă bonurile tale, și nici nu vrem să găzduim: ar însemna ca vânzările tale să treacă printr-un server al nostru, cu tot ce ține de asta. Dacă ai nevoie să comanzi casa din afara magazinului, serviciul de la mijloc îl construiești tu, pe infrastructura ta.
Partea noastră este să lucrăm curat cu el. Stația știe să meargă singură să-și ia de lucru, în două feluri:
| Cum | Ce faci tu | Când se potrivește |
|---|---|---|
| Coadă HTTPS | Expui două căi pe serviciul tău: una de unde stația ia comenzile care o așteaptă, alta unde îți trimite rezultatul. | Tipărire aproape instantanee. Calea obișnuită pentru un site. |
| SFTP | Lași fișierul de bon într-un folder pe serverul tău SFTP; stația îl ia de acolo, exact ca dintr-un folder local. | Când tipărirea poate întârzia un minut. Nimic de programat, doar un folder. |
În ambele, legătura pleacă dinspre magazin spre tine. Routerul magazinului rămâne închis, iar dacă serviciul tău cade, nu se strică nimic la casă: stația încearcă din nou mai târziu.
- HTTPS cu certificat valid. Nu ne conectăm pe http, și nu există bifă „acceptă orice certificat". Dacă certificatul nu se verifică, stația nu trimite nimic - nici măcar tokenul.
- Fără redirecționări către http. O redirecționare acceptată tacit este felul obișnuit în care o legătură criptată devine una în clar.
- Acreditarea în antet, niciodată în adresă: adresele ajung în jurnalele serverelor și în istoricul proxy-urilor.
- Cheie de idempotență obligatorie pe fiecare comandă. Aici, între tine și casă, este internetul - iar o comandă repetată fără cheie înseamnă al doilea bon fiscal.
- SFTP, nu FTP. FTP trimite parola în clar. Dacă asta pare complicat, este semn că adresa aia n-ar trebui să vadă o casă de marcat.
Știm că unele dintre astea îți fac implementarea mai grea. Nu deschidem portițe ca să fie mai ușor - la capătul lor stă aparatul care emite documente fiscale în numele unei firme.
Acceptul se dă de amândouă părțile
Cheia cu care se legitimează stația la serviciul tău o dai tu - este serviciul tău. Dar o cheie singură nu ajunge ca să înceapă să curgă bonuri: trebuie ca amândouă capetele să spună da.
- Tu înregistrezi stația în sistemul tău: NUI-ul casei și amprenta stației, pe care le vezi în aplicație. De aici încolo, serviciul tău știe exact cui îi dă de lucru.
- Omul din magazin aprobă legătura în Poleo: vede adresa serviciului, cine este și ce va putea face - doar tipărire, sau și citirea evidenței - și apasă „Permit".
- Omul alege și pe ce case. Aprobarea nu spune doar ce poți face, ci și unde: pe o stație cu două firme, integratorul uneia nu ajunge la casa celeilalte - nici să tipărească, nici să citească. Dacă ceri o citire fără să spui casa, primești refuz, nu datele întregii stații.
- Abia după ambele, stația începe să ia comenzi. Lipsește una - nu curge nimic, oricâte chei ar exista.
- Legătura se vede într-o listă, cu data la care a fost aprobată și de cine, și se oprește dintr-un clic. Oprirea este imediată și nu cere acordul celuilalt capăt.
Rostul este simplu: nici cineva care îți fură cheia nu poate tipări pe casa unui client fără ca omul de acolo să fi aprobat, și nici noi nu putem porni ceva la tine fără știrea ta.
Locația, ca să nu se încurce casele
Comanda ta spune locația, nu neapărat casa: numele scris în Setări → „Denumirea locului de muncă". Casele se schimbă, se strică, se înlocuiesc - locația rămâne. Dacă vrei un aparat anume, dai NUI-ul; dacă vrei „unde se poate", dai locația și tipărește stația aprobată de acolo.
Ce se vede, la fiecare capăt
- Fiecare comandă venită din afară apare în Istoricul aplicației, marcată cu serviciul de la care a venit - nu se amestecă cu bonurile trimise din magazin.
- Operatorul vede pe ecran că s-a tipărit ceva comandat de la distanță. Nimic nu iese pe hârtie „din senin".
- Tu primești același răspuns ca la API-ul local: numărul bonului fiscal, sau eroarea, cu aceleași coduri.
- Setările casei nu se pot schimba din afară. Prin coada ta trec exact operațiunile din acest manual, nici una în plus.
Erori - ce înseamnă și ce le rezolvă
Aici caută când ceva nu a mers. Fiecare eroare are codul ei, de ce apare și ce o rezolvă. Codurile sunt stabile - pe ele scrii logica; textul din message este scris pentru om și îl putem îmbunătăți oricând.
{ "ok": false,
"error": { "code": "device_error", "message": "Nu mai este hârtie în casa de marcat.",
"details": { … } } }
La 400 repari cererea - problema este în ce ai trimis tu. La 422 te duci la casa de marcat - cererea era bună, aparatul a refuzat. Diferența asta îți spune, fără să citești nimic altceva, dacă este cazul să sune cineva în magazin.
Erorile aplicației
unauthorized401Tokenul lipsește, este greșit, sau a fost regenerat între timp din Setări.
Ia tokenul curent din Setări → API și trimite-l în antetul Authorization: Bearer …. Dacă tocmai l-ai regenerat, cel vechi este mort din secunda aceea - nu există perioadă de grație, dinadins.
Încercările greșite sunt încetinite treptat, per adresă: prima costă o jumătate de secundă, a zecea câteva secunde. Nu blocăm adresa - un răuvoitor ar putea atunci să lase pe altcineva pe dinafară trimițând cereri greșite în numele lui - dar ghicitul devine costisitor și lasă urmă în jurnalul stației. Un token corect trece imediat, oricâte greșeli au fost înainte.
not_found404Calea nu există. De obicei lipsește prefixul de versiune sau este o greșeală de scriere.
Toate căile încep cu /api/v1/. Verifică și verbul: o cale de citire cerută cu POST răspunde 405, nu 404 - deci dacă primești 404, chiar nu există.
case_not_found404Casa din câmpul case nu există sau nu mai este activă. Se întâmplă des după ce cineva a dezactivat o casă din aplicație, iar programul tău încă trimite id-ul vechi.
Cere GET /cases și folosește un id de acolo. Dacă ai o singură casă, poți omite câmpul cu totul.
method_not_allowed405Ai cerut cu GET ceva ce se face cu POST (sau invers). Regula: ce citește din evidență este GET, ce cere aparatului este POST.
Schimbă verbul. Dacă voiai să vezi rapoartele deja tipărite, este GET /reports; POST /reports chiar tipărește unul.
bad_json400Corpul cererii nu este JSON valid. Cel mai des: o virgulă în plus la ultimul câmp, ghilimele „inteligente" copiate dintr-un document, sau corpul trimis fără antetul Content-Type: application/json.
message conține poziția exactă unde s-a rupt parsarea. Verifică și codificarea - trimite UTF-8.
bad_request400JSON-ul este valid, dar conținutul nu stă în picioare. Aici intră toate verificările pe care le facem înainte de a atinge hârtia:
- Plățile nu acoperă totalul. Mesajul îți dă diferența calculată. Cauza obișnuită: ai rotunjit tu totalul altfel decât se rotunjește pe bon - noi rotunjim fiecare linie la ban, jumătatea în sus, și abia apoi adunăm.
- Reducerile sunt mai mari decât marfa. Bonul ar ieși cu total zero sau negativ, iar casa nu poate închide așa ceva. Nu plafonăm reducerea și nu o reparăm - verifică valoarea sau produsele pe care se aplică.
- Cantitate zero. Un articol cu
qty: 0ar deveni un bon de 0 lei pe marfă dată din stoc. - Câmp cu valoare imposibilă:
kindcare nu esteXsauZ,amountnegativ la depunere,receipt_adjust.kindscris altfel decât cele patru forme.
În toate cazurile, pe hârtie nu a ieșit nimic și nu s-a consumat niciun număr de bon. Repari cererea și o retrimiți cu aceeași cheie de idempotență.
device_error422Cererea era bună; casa a refuzat. Motivul vine chiar de la aparat - vezi erorile casei, mai jos.
Nu retrimite în buclă. Majoritatea refuzurilor cer o intervenție fizică (hârtie, capac, raport Z), iar reîncercarea până atunci doar ține casa ocupată.
too_large413Cererea trece de 512 KB, sau ai cerut un fișier cu ?as=base64 și el trece de 8 MB.
Un bon, oricât de lung, are câteva zeci de KB - dacă ai depășit, aproape sigur trimiți altceva decât credeai. Pentru fișiere, cere-le fără as=base64: atunci vin în flux, oricât ar fi de mari.
file_missing404Documentul este în evidență, dar fișierul nu mai este pe disc: folderul a fost mutat, curățat de o unealtă de întreținere, sau discul este al altui calculator.
details îți dă calea căutată. Pentru jurnal, o salvare nouă aduce din nou fișierele pe care casa le mai are în memorie - dar numai pe alea; ce a ieșit din memoria ei nu se mai poate recupera decât de un tehnician autorizat.
io_error500Fișierul există, dar nu s-a putut citi: alt program îl ține deschis, sau drepturile pe folder nu ne lasă.
Verifică drepturile utilizatorului sub care rulează Poleo Fiscal pe folderul respectiv.
internal500Ceva la noi. Nu este vina cererii tale.
Trimite-ne ora exactă și seria casei (NUI). Dacă nu ai oprit transmiterea erorilor din Setări, cel mai probabil o avem deja în jurnalul nostru și putem lega cele două.
Erorile casei
Când primești device_error, motivul vine chiar de la aparat. Îl traducem: mai întâi întrebăm casa ce înseamnă codul ei, iar dacă nu răspunde, folosim tabelul oficial al producătorului - 324 de coduri pentru Datecs. Astea sunt cele pe care le vei întâlni cu adevărat.
Senzorul de hârtie s-a declanșat. Casa se blochează pe loc, chiar în mijlocul unui bon.
Pui rola și apeși C pe casă. Senzorul vede hârtia imediat, dar aparatul rămâne blocat până confirmi tu - nu putem confirma noi în locul tău, este o decizie de la aparat. Dacă bonul se rupsese la jumătate, el iese ANULAT și se retrimite.
Ultimul raport Z este mai vechi de o zi. Legea nu permite emiterea de bonuri peste această limită, deci casa refuză tot.
Un raport Z, și casa își reia lucrul. Nu îl facem automat: Z-ul închide ziua, consumă un loc în memoria fiscală și are urmări contabile - alegerea este a comerciantului, noi doar îi spunem că asta este problema.
Capacul rolei nu este închis complet. Se întâmplă des după schimbarea hârtiei.
Îl închizi apăsat, până se aude clicul, și apeși C.
Un bon a rămas început și neterminat - de obicei după o pană de curent sau o deconectare în mijlocul plății.
POST /void îl închide. Iese ANULAT pe hârtie, apoi vânzarea se reia curat.
Casa a primit o comandă corectă, dar nepotrivită cu ce face ea în clipa aceea: un raport cerut cu bonul deschis, un export cerut înainte de primul Z, o vânzare într-un bon neînceput.
Cere GET /cases/{id}/status ca să vezi în ce stare e, rezolvă starea, apoi reia. Dacă a rămas un bon deschis, POST /void.
Numărul de rapoarte Z pe care îl poate ține memoria s-a epuizat. E o limită fizică a aparatului.
Service autorizat. Memoria fiscală se înlocuiește, nu se golește - iar până atunci casa nu mai emite. Descarcă jurnalul electronic înainte de intervenție.
Cablul, portul COM renumerotat de Windows, IP schimbat de router, sau bluetooth-ul deconectat. Aparatul poate fi perfect sănătos.
Aplicația încearcă singură să regăsească aparatul (alt port COM, alt IP în aceeași rețea) și își actualizează configurația când îl găsește. Dacă nu reușește, bonul nu se pierde: intră în coadă și se tipărește singur când casa revine - îl vezi cu status: "pending" în GET /receipts.
Când răspunsul este 200 și tot nu este ce credeai
Trei situații care nu sunt erori, dar se citesc greșit ușor.
"repeated": trueAi retrimis o cerere cu o cheie de idempotență folosită deja. Primești răspunsul primei încercări, cu același receipt_no.
Asta este comportamentul dorit - pe hârtie nu a ieșit nimic a doua oară. Dacă chiar voiai un bon nou, dă-i altă cheie.
"online": falseGET /cases/{id}/status răspunde 200 chiar și când aparatul este deconectat: întrebarea a primit un răspuns, iar răspunsul este „nu este conectat".
Uită-te la online, nu la codul HTTP. Motivul este în message.
"status": "pending"Bonul a fost primit și pus în coadă, dar încă n-a ieșit pe hârtie - casa era ocupată sau deconectată.
Nu-l retrimite. Se tipărește singur când casa revine, iar în meniul aplicației, lângă Istoric, apare un număr cu câte documente așteaptă.
Limite
| Ce | Cât | De ce |
|---|---|---|
| Corpul unei cereri | 512 KB | Un bon foarte lung tot are câteva zeci de KB. |
| Conținut citit (jurnal) | 8 MB | Peste atât, iei fișierul de pe disc. |
limit pe liste |
500 | O listă nemărginită ține firul minute întregi. |
| Operațiuni simultane pe o casă | 1 | Aparatul servește un singur client odată. Cererile se așează la rând singure. |
Nu paraleliza cererile către aceeași casă și nu interoga starea în buclă strânsă. Nu se strică nimic - se așează la rând - dar un bon care aștepta după zece interogări de stare este tipărit mai târziu decât ar trebui. În situații specifice este posibil să se blocheze aparatul fiscal.
