REST API pentru aplicații mobile, integrări POS și scripturi externe.
http://localhost/Efactura/public/api/v1https://facturi.cloud/api/v1v1application/json; charset=utf-8)Authorization: Bearer <token> (Personal Access Token)Toate scrierile sunt scoped la o singură companie — token-ul are atașată
compania activă. Dacă utilizatorul are acces la mai multe companii, poate
emite token-uri separate pentru fiecare.
---
API-ul folosește Personal Access Tokens (PAT) opace. Generarea se face din panoul web la Setări → Token-uri API.
Authorization: Bearer fcl_AbCd1234_...veryLongOpaqueToken| Scope | Descriere |
|---|---|
documents:read | Citește facturi, proforme, avize. |
documents:write | Creează / modifică facturi, proforme, avize. |
clients:read | Listare/detalii clienți + lookup ANAF. |
clients:write | Creare/modificare clienți. |
products:read | Listează produse, serii. |
products:write | Creează/modifică produse. |
pos:read | Listează case de marcat, bonuri, sesiuni, rapoarte Z. |
pos:write | Emite/anulează bonuri, deschide/închide sesiuni, raport Z. |
Scope-ul:writeinclude și:readpentru același domeniu.
// Succes
{
"data": { ... },
"meta": { "page": 1, "per_page": 25, "total": 142, "last_page": 6 },
"errors": []
}
// Eroare
{
"data": null,
"meta": null,
"errors": [
{ "code": "validation_error", "message": "Lipsește `id_client`.", "field": "id_client" }
]
}| Cod | Înțeles |
|---|---|
| 200 | OK |
| 201 | Resursă creată |
| 400 | Cerere invalidă (JSON malformat, header lipsă) |
| 401 | Token lipsă / invalid / expirat |
| 403 | Token valid dar fără scope sau companie inaccesibilă |
| 404 | Resursă inexistentă |
| 409 | Conflict (sesiune deja deschisă etc.) |
| 422 | Eroare validare business |
| 429 | Rate limit depășit |
| 500 | Eroare server (jurnalizată) |
---
/healthVerifică conectivitatea la baza de date și starea serviciului.
curl https://app.facturi.cloud/api/v1/health{
"data": { "status": "ok", "service": "facturi.cloud-api", "version": "v1",
"database": "ok", "time": "2026-04-19T18:00:00+03:00" },
"meta": null,
"errors": []
}---
/meDetalii utilizator + listă companii la care are acces + scope-uri token.
/companii/activeDetaliile companiei active (CUI, adresă, regim TVA, plan abonament).
---
/facturiListare paginată. Suportă filtre: status, id_client, from, to, q (text).
curl -H "Authorization: Bearer $TOKEN" \
"https://.../api/v1/facturi?status=emisa&page=1&per_page=25"/facturi/{id}Bundle complet: header + linii + plăți + e-Factura status.
/facturiCreează factură (draft sau emite imediat). Poate folosi produse din catalog, crea produse noi pe linie, sau liniile pot fi text liber.
Scope: documents:write (crearea produselor din payload-ul facturii nu cere separat products:write).
{
"id_serie": 12, // optional — fallback pe seria default
"id_client": 345, // sau bloc `client` (vezi mai jos)
"client": { // alternativă: creează / reutilizează client după CUI
"denumire": "ACME SRL",
"cui": "RO12345678",
"adresa": "Str. Test 1",
"judet": "B",
"localitate": "București"
},
"moneda": "RON",
"tip_factura": "380", // 380 = comercială (default), 751 = în baza bonului
"data_emitere": "2026-04-19",
"data_scadenta": "2026-05-04",
"modalitate_plata": "OP / Card / Cash",
"observatii": "Plata în 15 zile",
"linii": [
// 1) Produs existent din catalog — denumire/preț/TVA se completează automat dacă lipsesc
{ "id_produs": 5, "cantitate": 2 },
// 2) Linie text liber (nu salvează în catalog)
{ "descriere": "Servicii consultanță", "cantitate": 1, "pret_unitar": 350.00, "cota_tva": 21 },
// 3) Produs nou în catalog (sau reutilizat dacă există același cod_produs)
{
"creeaza_produs": true,
"denumire": "Mouse wireless",
"cod_produs": "MW-01",
"unitate_masura": "buc",
"pret_unitar": 89.00,
"cota_tva": 21,
"cantitate": 3
},
// 4) Echivalent: bloc nested `produs`
{
"produs": {
"denumire": "Licență software",
"tip": "serviciu",
"pret_fara_tva": 500.00,
"cota_tva": 21,
"unitate_masura": "buc"
},
"cantitate": 1
}
],
"scade_stoc": false,
"status": "emisa" // sau "draft" — implicit: "emisa"
}Câmpuri pe linie
| Câmp | Note |
|---|---|
id_produs | Opțional. Produs din catalog. |
descriere / denumire | Obligatoriu dacă nu există id_produs / produs. |
cantitate | Implicit 1 |
pret_unitar / pret | Dacă lipsește și există produs, se ia prețul din catalog |
cota_tva / tva | Implicit 21% (0% dacă firma e neplatitor de TVA) |
discount_procent / discount_pct | |
discount_valoric / discount_val | |
creeaza_produs | true → salvează în catalog (sau găsește după cod_produs) |
produs | Obiect cu denumire, cod_produs, tip, unitate_masura/um, pret_fara_tva, cota_tva |
Pentru catalog separat:GET/POST /produse(scopeproducts:read/products:write).
/facturi/{id}/pdfReturnează PDF-ul (Content-Type: application/pdf).
/facturi/{id}/anafTrimite factura în SPV (e-Factura).
/facturi/{id}/incasareÎnregistrează o încasare pe factură.
{ "data": "2026-04-20", "suma": 238.00, "metoda": "card", "observatii": "" }/facturi/{id}/stornoEmite factură de storno (negativă) pentru anularea facturii curente. Funcționează și pentru facturi deja trimise în SPV: se creează un document nou, se emite și se încearcă trimiterea automată în e-Factura (dacă nu e exclusă din SPV). Răspunsul include eventual spv cu rezultatul încărcării.
---
CRUD similar cu facturile, fără integrare ANAF/scădere stoc.
GET /proforme — listare paginatăGET /proforme/{id} — detaliiPOST /proforme — creareGET /proforme/{id}/pdf — PDFPOST /proforme/{id}/factura — convertește proforma în factură (păstrează liniile)---
GET /avize — listareGET /avize/{id} — detalii (header + linii)POST /avize — creareGET /avize/{id}/pdf — PDFPOST /avize/{id}/factura — convertește avizul în facturăAvizele NU se trimit în SPV (formularul e doar tipizat fiscal pe transport).
---
GET /clienti?q=...&page=1 — listare cu căutare după denumire/CUIGET /clienti/{id} — detaliiPOST /clienti — crearePOST /clienti/{id} — actualizareGET /clienti/lookup-anaf?cui=RO12345678 — lookup în registrul ANAF (rate-limited 30/min)---
Scope: products:read / products:write
GET /produse?q=...&tip=produs|serviciu&activ=1 — listare paginatăGET /produse/{id} — detaliiPOST /produse — creare (denumire obligatoriu; unitate_masura, pret_fara_tva, cota_tva default 21, tip)POST /produse/{id} — actualizareGET /serii?tip=factura — listare serii (factura/proforma/aviz/chitanta)Exemplu creare produs:
{
"denumire": "Cablu HDMI 2m",
"cod_produs": "HDMI-2",
"tip": "produs",
"unitate_masura": "buc",
"pret_fara_tva": 45.00,
"cota_tva": 21
}Alternativ, poți crea produse direct pePOST /facturicucreeaza_produs/produspe linie.
---
Status v1: schema DB + driver mock funcțional + endpoint-uri complete.
Driverele AMEF reale (Datecs, Tremol, Custom) sunt stub-uri ce vor fi
implementate într-o versiune ulterioară. Pentru integrări proprii, casa
de marcat poate fi setată cu driver = 'mock' în dev.Fără token pre-creat în web: utilizatorul se autentifică în app cu email/parolă, primește un PAT cu scope-uri POS și alege / creează casa de marcat.
POST /pos/pair — public (fără Bearer) {
"email": "casier@firma.ro",
"password": "••••••••",
"id_companie": 5, // obligatoriu dacă userul are mai multe firme
"id_casa": 2, // SAU selectează o casă existentă
"casa_nume": "Casa magazin", // SAU creează / refolosește după nume
"driver": "mock", // opțional la creare: mock | datecs | tremol | custom
"device_name": "Tablet POS Magazin 1"
} Scope-uri emise automat: pos:read, pos:write, products:read.
Răspuns 200:
{
"data": {
"token": "fcl_........",
"scopes": ["pos:read", "pos:write", "products:read"],
"user": { "id": 6, "email": "...", "nume_complet": "..." },
"company": { "id": 5, "denumire": "...", "cui": "..." },
"casa": { "id": 2, "nume": "Casa magazin", "driver": "mock", "activ": true },
"case_marcat": [ /* lista firmă */ ],
"gestiuni": [ /* pentru sync stoc */ ]
}
} Coduri utile: - 409 + multiple_companies → alegeți id_companie din data.companies - 409 + casa_required → alegeți id_casa din data.case_marcat sau trimiteți casa_nume
GET /pos/case-marcat — listare case configurate (filtru opțional ?activ=1)POST /pos/case-marcat — creează casă (nume obligatoriu; driver opțional) — scope pos:writeGET /pos/case-marcat/{id} — detaliiGET /pos/case-marcat/{id}/status — query online la AMEF (driver) + cache statusGET /pos/sesiuni/curenta?id_casa=1 — sesiunea deschisă curentă (sau null)POST /pos/sesiuni/deschide — deschide o sesiune { "id_casa": 1, "sold_initial": 100.00, "observatii": "Schimb tura 1" }POST /pos/sesiuni/inchide — închide sesiunea { "id_sesiune": 42, "sold_final": 1234.50 }
// sau
{ "id_casa": 1 } // închide sesiunea deschisă curentăGET /pos/bonuri?id_casa=&id_sesiune=&status=&from=&to=&page=GET /pos/bonuri/{id} — bundle (header + linii + plăți)POST /pos/bonuri — emite bon (auto_emit=true implicit) {
"id_casa": 1, // optional — fallback pe prima casă activă
"id_sesiune": 42, // optional — fallback pe sesiunea deschisă
"id_client": null, // optional — pentru bon nominal
"moneda": "RON",
"linii": [
{ "descriere": "Cafea espresso", "cantitate": 1, "pret_unitar": 8.00, "cota_tva": 9, "departament": "Bauturi" },
{ "id_produs": 22, "cantitate": 2, "pret_unitar": 5.00, "cota_tva": 19 }
],
"plati": { "cash": 20.00, "card": 0, "alt": 0 },
"auto_emit": true // dacă false, bonul rămâne în status "pregatit"
}Răspuns:
{
"data": {
"id": 101,
"status": "emis",
"numar_bon": 5,
"serie_fiscala": "MOCK-000005",
"subtotal": 18.00, "total_tva": 1.72, "total": 19.72,
"plati": { "cash": 20.00, "card": 0, "alt": 0 },
"rest": 0.28,
"linii": [ ... ],
"emit": { "numar_bon": 5, "serie_fiscala": "MOCK-000005", "message": "Bon mock emis." }
}
}POST /pos/bonuri/{id}/anulare — anulează bonul (driver: cancel sau bon negativ) { "motiv": "Greșeală operator" }GET /pos/rapoarte-z?id_casa=&from=&to= — listare paginatăGET /pos/rapoarte-z/{id} — detalii raportPOST /pos/rapoarte-z — închide ziua fiscală { "id_casa": 1, "id_sesiune": 42 }---
Accept: application/json.Content-Type: application/json.meta.last_page pentru paginare; nu presupune o singură pagină.(debounce buton de "Emite" 1500 ms) — server-ul nu are deduplicare automată.
---
Autentificare: Authorization: Bearer {ANAF_PICKUP_API_SECRET} (din .env platformă). Nu folosește personal access token-urile mobile.
POST /anaf/pickup-linkGenerează link public OAuth pentru un CUI.
Body JSON: { "cui": "RO12345678" }
Răspuns 201:
{
"data": {
"id": 1,
"cui": "RO12345678",
"public_token": "...",
"url": "https://facturi.cloud/anaf/preluare/...",
"status": "pending",
"link_expires_at": "2026-08-30 14:00:00",
"created_at": "2026-08-28 14:00:00"
},
"meta": null,
"errors": []
}Flux: afișați / trimiteți url utilizatorului → el apasă „Preluare token” (OAuth) → apoi aplicația apelează GET /anaf/pickup-token.
GET /anaf/pickup-status?cui=RO12345678Statusul ultimului link (fără a consuma tokenul). has_token: true când e gata.
GET /anaf/pickup-token?cui=RO12345678One-shot: returnează access_token / refresh_token / expira_la și șterge rândul. 404 dacă nu e ready. Nu reîncercați după un 200.
---
Retry-After recomandat. - GET /clienti/lookup-anaf → 30 cereri/minut/token
---
Vezi tools/api_smoke_test.php. Setează API_BASE_URL și API_TOKEN:
$env:API_BASE_URL = "http://localhost/Efactura/public/api/v1"
$env:API_TOKEN = "fc_pat_..."
php tools/api_smoke_test.phpScriptul rulează: /health, /me, /companii/active, /serii, /clienti?per_page=1, /facturi?per_page=1, /proforme?per_page=1, /avize?per_page=1, /produse?per_page=1, /pos/case-marcat. Output: tabel cu status / latency.
---
Vezi docs/api-v1.postman_collection.json. Importă în Postman, setează variabilele de collection: baseUrl și token.