API v1
Versiunea v1 Portul 9111

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

  1. 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".
  2. Copiază tokenul. Tot în Setări → API. Se generează singur la prima pornire și rămâne același până îl schimbi tu.
  3. Verifică legătura. Cere /api/v1/health - dacă îți răspunde, restul e doar chestiune de a trimite JSON-ul potrivit.
Primul contactnu are nevoie de token
curl http://127.0.0.1:9111/api/v1/health
La ce să te aștepți

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 /methodsCe știe versiunea instalată. O stație mai veche poate să nu aibă ultimele metode - afli din cod, nu dintr-o eroare la client.
GET /casesCasele configurate și NUI-ul fiecăreia. Pe NUI individualizezi casa, nu pe denumire și nu pe id.
GET /cases/{nui}/configCotele 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 /terminalsTerminalele de card și, în can, ce operațiuni au: retur, anulare, închidere de zi.
GET /settingsCum 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.

Ce rămâne al omului, și de ce

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

  1. GET /health - există serviciul? (nu cere token)
  2. GET /cases - ia NUI-ul casei și pune-l în fiecare cerere, la case.
  3. GET /cases/{nui}/config - ia cotele și sloturile, și potrivește-ți nomenclatorul pe ele.
  4. POST /receipts cu cheie de operațiune - primul bon.
  5. 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.

Pornire45 de exemple
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
La ce sa te astepti
  • 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

Un singur fișier, nimic de instalat.

PHP

7.4+, fără composer

Fără framework. Merge și sub XAMPP.

C

zero dependințe pe Windows

Transport pe WinHTTP. Merge și din C++.

Rulare și compilareaceleași exemple, aceleași numere
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
La ce sa te astepti
  • 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++, are extern "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, id sau status. 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.
Portal web: cine cheamă casa depinde de unde stă omul

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:

Antetul cerut
Authorization: Bearer TOKENUL_DIN_SETARI

Dacă îți este mai la îndemână, merge și antetul X-Poleo-Token cu aceeași valoare.

Tokenul este cheia casei de marcat

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ă.

La ce să te aștepți

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:

Reușită
{ "ok": true, "receipt_no": "958", "message": "OK" }

Când n-a mers:

Eșec
{ "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.

Aceeași cerere, trimisă de două ori
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": [...]}}'
La ce să te aștepți
  • Prima cerere: 200, cu receipt_no - bonul a ieșit pe hârtie.
  • A doua, cu aceeași cheie: 200, același receipt_no, plus "repeated": true. Pe hârtie nu este tipărit nimic.
Cheia identifică operațiunea, nu momentul

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?

GET/api/v1/health

Singura cale care nu cere token. Serviciul spune cine e, ca să poți verifica legătura înainte să ai credențiale.

Cerere
curl http://127.0.0.1:9111/api/v1/health
Răspuns
{ "ok": true, "service": "poleo-fiscal", "api": "v1" }
La ce să te aștepți

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

GET/api/v1/cases

Lista caselor pe care le poți comanda. De aici iei id-ul, dacă lucrezi cu mai multe aparate.

Cerere
curl http://127.0.0.1:9111/api/v1/cases \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "ok": true, "cases": [ { "id": "01M04R5GFS7N3BAMP7JBMWTAZ4", "name": "DB9999999999",
               "brand": "datecs", "model": "DP-25",
               "serial": "DB9999999999", "transport": "tcp" } ] }
La ce să te aștepți

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.

Individualizează după NUI, nu după id

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

GET/api/v1/cases/{id}/status

Întreabă aparatul, nu evidența: răspunde dacă este conectat, ce serie are și ce semnalează (hârtie, capac, memorie fiscală).

Cerere
curl http://127.0.0.1:9111/api/v1/cases/DB9999999999/status \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "ok": true, "online": true, "message": "Conectat", 
            "info": { "model": "DP-25", "serie": "DB9999999999", "cif": "RO99999999", 
                      "flags": { "paper_end": false, "cover_open": false } } }
La ce să te aștepți
  • 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 200 cu "online": false și motivul în message.
  • 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

POST/api/v1/receipts

Operațiunea centrală. Trimiți articolele și plățile, primești numărul bonului fiscal.

Cerereapă plată, 2 buc × 3,50
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 } ]
    }
  }'
Răspuns
{ "ok": true, "receipt_no": "958", "message": "OK" }
La ce să te aștepți
  • 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_no este 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.

Cotele se citesc din casă, nu se presupun

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.

Dacă le trimiți pe amândouă

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.
Plățile trebuie să acopere totalul

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:

Reducere de 10% pe tot bonul
{ "receipt": { "items": [ … ], "receipt_adjust": { "kind": "disc_pct", "value": 10 },
              "payments": [ … ] } }
La ce să te aștepți
  • 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.
Bon emis pe firmă
{ "receipt": { "client": "EXEMPLU COMERT SRL", "client_vat": "RO99999999",
              "items": [ … ], "payments": [ … ] } }
La ce să te aștepți
  • 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ă.

Cerereîn același bon
{ "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âmpValoriCe face
textpână la 64 de caractereRândul propriu-zis. Mai lung, se taie.
boldtrue / falseÎngroșat.
italictrue / falseÎnclinat.
underlinetrue / falseSubliniat.
double_heighttrue / falseÎnălțime dublă.
alignleft, center, rightAlinierea pe lățimea bonului.

Coduri de bare

typeCe acceptă barcode
EAN8exact 8 cifre
EAN13exact 13 cifre
CODE1283-31 caractere ASCII
QR3-279 caractere ASCII
I2OF53-22 cifre
PDF4173-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.

La ce să te aștepți
  • 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.
Nu toate casele știu

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

POST/api/v1/reports
Cerere
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"}'
Răspuns
{ "ok": true, "receipt_no": "28", "message": "OK" }
La ce să te aștepți
  • "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_no este numărul raportului Z.
  • Orice altceva la kind întoarce 400, fără să atingă casa.
Z-ul este definitiv

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.

Cerere
curl -X POST http://127.0.0.1:9111/api/v1/reports \
  -H "Authorization: Bearer TOKENUL" \
  -H "Content-Type: application/json" \
  -d '{"kind": "DEPARTAMENTE"}'
kindCe iese pe hârtie
D / DEPARTAMENTERulajul pe departamente.
G / GRUPERulajul pe grupele de articole.
OPERATORIRulajul pe operatori (casieri).
PLU / ARTICOLEArticolele programate în casă, cu rulajele lor.
La ce să te aștepți
  • 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.
Licența se aplică și aici

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

POST/api/v1/cash
Cererefond de rulaj, 200 lei
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"}'
La ce să te aștepți
  • "in" = depunere, "out" = retragere. Altceva → 400.
  • Suma trebuie să fie mai mare ca zero; reason este 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

POST/api/v1/drawer
Cerere
curl -X POST http://127.0.0.1:9111/api/v1/drawer \
  -H "Authorization: Bearer TOKENUL" -d '{}'
La ce să te aștepți

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

POST/api/v1/copy
La ce să te aștepți

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

POST/api/v1/void
La ce să te aștepți

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.

Asta NU anulează un bon deja tipărit

/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

  1. 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ță.
  2. 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ă.
  3. Se întocmește declarația. Numerotată continuu, în format PDF, cu bonul, suma, motivul și gestionarul. Rămâne în registrul din aplicație.
  4. 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.
  5. 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.
Nu se mai cere parolă și nici aprobare pe email

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.

Prin API
  • 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

POST/api/v1/declarations

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.

Cerere
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}'
La ce să te aștepți
  • acknowledged: true este 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ști 400.
  • receipt este id-ul bonului din evidență (îl ai din /receipts), nu numărul fiscal.
  • reasons se ia din GET /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

POST/api/v1/returns

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.

Bonul de storno NU există

Î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ă.

Cereretot bonul
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}'
Cereredoar un produs
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}]}'
Răspuns
{ "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" } }
La ce să te aștepți
  • acknowledged: true și reason sunt 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_no este 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 400 cu 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ști 422. 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

GET/api/v1/returns

Lista retururilor, cu starea fiecăruia. Un retur anume, cu liniile lui, la GET /api/v1/returns/{id}.

Cerere
curl http://127.0.0.1:9111/api/v1/returns \
  -H "Authorization: Bearer TOKENUL"
Stările
  • 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

GET/api/v1/settingsPOST/api/v1/settings

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.

Ce nu se schimbă de aici, și de ce

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.

Cererece se poate schimba
curl http://127.0.0.1:9111/api/v1/settings   -H "Authorization: Bearer TOKENUL"
Răspuns
{ "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…" } ] }
Cerereschimbă
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"}}'
Răspunsparțial
{ "ok": true,
  "changed": { "bon.sertar_mereu": true },
  "rejected": { "api.port": "Portul serviciului nu se schimbă din serviciu…" },
  "message": "o setare schimbată, una refuzată." }
La ce să te aștepți
  • 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.
  • 200 doar dacă a intrat ceva. Dacă nimic nu a trecut, primești 400 cu aceleași două liste în details - un 200 gol ar trece nevăzut prin codul tău.
  • Tipurile: bifa (acceptă true/false, 1/0, "on"), numar (cu interval), lista (cu options î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ște sftp.key, calea către cheia privată, și atunci nu mai stă nicio parolă acolo.
  • Tipul ora vrea "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

GET/api/v1/companyPOST/api/v1/company

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.

Cerere
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"}'
La ce să te aștepți
  • 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

POST/api/v1/pos/refund

Trimite banii înapoi la client, pe cardul lui. Marfă returnată, serviciu neprestat, încasare făcută greșit.

Cerere
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}'
La ce să te aștepți
  • 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

POST/api/v1/pos/void

Scoate o plată din borderoul curent al terminalului, ca și cum nu ar fi existat.

CerereSTAN + suma exactă
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}'
La ce să te aștepți
  • stan este 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

POST/api/v1/pos/settlement

Trimite borderoul la bancă și pornește decontarea.

Cerere
curl -X POST http://127.0.0.1:9111/api/v1/pos/settlement \
  -H "Authorization: Bearer TOKENUL" \
  -H "Idempotency-Key: settlement-2026-08-17"
La ce să te aștepți
  • 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

HTTP 501error: unsupported

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.

Răspuns
{ "ok": false,
  "error": { "code": "unsupported",
              "message": "Returul pe card nu este suportat de banca_x T100",
              "details": { "bank": "banca_x", "model": "T100" } } }
De ce un cod separat, și nu 422
  • 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.
  • details spune 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

POST/api/v1/anaf
Cerere
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"}'
La ce să te aștepți
  • Fișierul se scrie în folderul de export al casei; message spune unde.
  • Poți da out_dir ca 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

GET/api/v1/receipts

Din evidența aplicației, nu de pe casă - deci nu blochează aparatul în mijlocul unei vânzări.

Cerere
curl "http://127.0.0.1:9111/api/v1/receipts?limit=3&from=2026-08-16" \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "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.
La ce să te aștepți

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

GET/api/v1/receipts/{id}
La ce să te aștepți

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

GET/api/v1/reports
Cereredoar rapoartele Z
curl "http://127.0.0.1:9111/api/v1/reports?kind=Z&limit=5" \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "ok": true,  "reports": [ { "kind": "Z", "report_no": 28, "total": 115683.06,
                 "ts": "2026-08-16 17:18:45" } ] }
La ce să te aștepți

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

GET/api/v1/totals
Răspuns
{ "ok": true,
  "today": { "bonuri": 99, "total": 229938.75, "tva": 39824.04, "avg": 2322.61,
              "z_count": 3, "last_z": "17:18" } }
La ce să te aștepți

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

GET/api/v1/problems

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.

Răspuns
{ "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": [ … ] }
La ce să te aștepți
  • 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) sau alert(in aplicație este un mesaj pe fond roșu, ceva a "crăpat").
  • kind este stabil și se poate verifica în cod: paper_end, cover_open, fm_full, queue, op_error

Jurnalul electronic - salvările

GET/api/v1/je

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.

Cerere
curl "http://127.0.0.1:9111/api/v1/je?limit=3" \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "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" } ] }
La ce să te aștepți
  • O salvare nu este un fișier, ci o vizită la casă: aducem fișierele zilelor fiscale care lipseau din folderul casei. files spune câte a adus atunci.
  • ok: false înseamnă că vizita a eșuat, iar message spune 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ă

GET/api/v1/je/{id}

Salvarea, plus lista fișierelor din folderul casei ei: nume, mărime, data ultimei scrieri.

Cerere
curl http://127.0.0.1:9111/api/v1/je/01M04VA5GX7SY883TVE3SR2DRS \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "ok": true,
  "export": { "id": "01M04VA5GX…", "files": 23, … },
  "files": [
    { "name": "2026-08-16_1234567890_Z0026.p7b",
      "size": 2084, "modified": "2026-08-16 11:34:17" } ] }
La ce să te aștepți
  • 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 .p7b sunt 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 404 cu not_found.

Jurnalul electronic - descarcă un fișier

GET/api/v1/je/{id}/files/{nume}

Fișierul însuși, exact așa cum l-a semnat casa. Aici nu primești JSON, ci documentul.

Cereresalvează pe disc
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"
Răspuns
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>
La ce să te aștepți
  • 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ă.
Cât ține casa jurnalul

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

GET/api/v1/declarations

Registrul declarațiilor întocmite în aplicație pentru bonurile emise greșit, cu PDF-ul fiecăreia.

Cerere
curl http://127.0.0.1:9111/api/v1/declarations \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "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" } ] }
La ce să te aștepți
  • seq_no este numărul din registru - același care apare pe PDF. Numerotarea este continuă și nu se reia.
  • cash_out spune 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și Content-Disposition, același ?as=base64).

Tot ce știm despre o casă

GET/api/v1/cases/{nui}/config

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ă.

Cerere
curl http://127.0.0.1:9111/api/v1/cases/DB9999999999/config \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "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" } ] } } }
La ce să te aștepți
  • device.vat sunt cotele programate în casă, pe grupe. Ele sunt adevărul după care tipărește ea - dacă vrei să trimiți vat_group, de aici îl iei, nu din presupuneri.
  • device.payments sunt 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_dialect spune cum se citesc numerele pe care le trimiți tu în index: 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, cu device.read_ok: false și motivul în device.message - restul configurării, cea de la noi, este oricum acolo.

Licența

GET/api/v1/license

Câte zile mai are fiecare casă, în ce stare este și cât mai poate lucra fără internet.

Cerere
curl http://127.0.0.1:9111/api/v1/license \
  -H "Authorization: Bearer TOKENUL"
Răspuns
{ "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)." } ] }
La ce să te aștepți
  • 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, blocked sau error. Pe ok: false casa nu mai tipărește - este singura stare care oprește vânzarea.
  • În probă, bons_left spune 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ă este null, 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

GET/api/v1/terminals

Terminalele bancare configurate, cu id-urile lor. De aici iei valoarea pentru terminal la plată.

Răspuns
{ "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 } } ] }
La ce să te aștepți
  • routing spune 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 /pos va fi refuzată până se bifează în Setări.
  • can spune 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ăspunde 501 cu error: "unsupported" oricând ai încerca. Soluția este alt terminal sau alt contract cu banca, nu o reîncercare.

Plătește cu cardul

POST/api/v1/pos

Trimite suma la terminalul bancar și așteaptă răspunsul băncii. Separat de bon, și dinadins.

Datele de card nu trec pe aici

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.

Cererepe un terminal anume
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"}'
Răspuns
{ "ok": true, "message": "Aprobat",
  "payment": { "approved": true, "auth": "004512",
                "rrn": "622812345678", "card": "****4471", "terminal": "Terminal casa 1" } }
La ce să te aștepți
  • Î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 spune routing. 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 și rrn: 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.

  1. Poleo Fiscal rulează pe calculatorul din magazin, lângă casă, cu API-ul pornit din Setări → API.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Exemplu: comandă plătită online, bon tipărit în magazin
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 } ] } }
La ce să te aștepți
  • Bonul iese pe casa cu NUI-ul DB9999999999, iar tu primești numărul lui fiscal în receipt_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" în GET /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

GET/api/v1/station

Ce treci în sistemul tău ca să înregistrezi stația: amprenta ei, locația și casele pe care le poate comanda.

Răspuns
{ "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" } ] } }
La ce să te aștepți
  • 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.
  • location este numele din Setări → „Denumirea locului de muncă". După el rutezi comenzile: casele se schimbă, locul rămâne.

Ce știe versiunea asta

GET/api/v1/methods

Lista metodelor, ca să afli din cod ce poate stația - fără să ghicești după manual ce s-a mai adăugat.

Răspuns
{ "ok": true, "api": "v1", "methods": [
    { "method": "POST", "path": "/api/v1/receipts",
      "description": "Tipărește un bon fiscal" }, … ] }
La ce să te aștepți

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.

CaleaCe 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.
Plata incertă

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ă

POST/api/v1/reports

Același drum ca X și Z, cu kind: "PERIODIC" și un interval.

Cerere
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"}'
La ce să te aștepți
  • Iese pe hârtie raportul pe intervalul cerut, citit din memoria fiscală a casei.
  • detailed: false cere 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.

Nimeni nu deschide o poartă spre casa de marcat

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:

CumCe faci tuCâ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.

Ce cerem de la serviciul tău, fără excepții
  • 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.

  1. 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.
  2. 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".
  3. 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.
  4. Abia după ambele, stația începe să ia comenzi. Lipsește una - nu curge nimic, oricâte chei ar exista.
  5. 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.

Forma unei erori
{ "ok": false,
  
    "error": { "code": "device_error", "message": "Nu mai este hârtie în casa de marcat.",
              "details": { … } } }
Prima întrebare: 400 sau 422?

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

unauthorized401

Tokenul 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_found404

Calea 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_found404

Casa 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_allowed405

Ai 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_json400

Corpul 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_request400

JSON-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: 0 ar deveni un bon de 0 lei pe marfă dată din stoc.
  • Câmp cu valoare imposibilă: kind care nu este X sau Z, amount negativ la depunere, receipt_adjust.kind scris 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_error422

Cererea 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_large413

Cererea 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_missing404

Documentul 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_error500

Fiș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.

internal500

Ceva 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.

Nu mai este hârtie

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.

Ziua fiscală depășește 24 de ore

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 este deschis

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.

Plată deja inițiată / bon deschis

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.

Comandă ilegală / nepermisă în starea curentă

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.

Memoria fiscală este plină

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.

Casa nu răspunde

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": true

Ai 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": false

GET /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.
O casă, un vorbitor

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.

Poleo Fiscal · API v1. Manualul descrie versiunea v1. Ne vom strădui să nu aducem modificări care să afecteze funcționalitatea softului pe care îl dezvolți ci mereu să adăugăm și / sau rafinăm metoda într-o manieră compatibilă de la o versiune la alta.

poleo.ro · Toată documentația