facturi.cloud API

Base URL: https://facturi.cloud/api/v1

facturi.cloud — REST API v1

REST API pentru aplicații mobile, integrări POS și scripturi externe.

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.

---

1. Autentificare

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

1.1 Scope-uri suportate

ScopeDescriere
documents:readCitește facturi, proforme, avize.
documents:writeCreează / modifică facturi, proforme, avize.
clients:readListare/detalii clienți + lookup ANAF.
clients:writeCreare/modificare clienți.
products:readListează produse, serii.
products:writeCreează/modifică produse.
pos:readListează case de marcat, bonuri, sesiuni, rapoarte Z.
pos:writeEmite/anulează bonuri, deschide/închide sesiuni, raport Z.
Scope-ul :write include și :read pentru același domeniu.

1.2 Răspunsuri standard

// 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" }
  ]
}

1.3 Coduri HTTP relevante

CodÎnțeles
200OK
201Resursă creată
400Cerere invalidă (JSON malformat, header lipsă)
401Token lipsă / invalid / expirat
403Token valid dar fără scope sau companie inaccesibilă
404Resursă inexistentă
409Conflict (sesiune deja deschisă etc.)
422Eroare validare business
429Rate limit depășit
500Eroare server (jurnalizată)

---

2. Endpoint-uri publice

2.1 GET /health

Verifică 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": []
}

---

3. Profil & companii

3.1 GET /me

Detalii utilizator + listă companii la care are acces + scope-uri token.

3.2 GET /companii/active

Detaliile companiei active (CUI, adresă, regim TVA, plan abonament).

---

4. Facturi

4.1 GET /facturi

Listare 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"

4.2 GET /facturi/{id}

Bundle complet: header + linii + plăți + e-Factura status.

4.3 POST /facturi

Creează 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âmpNote
id_produsOpțional. Produs din catalog.
descriere / denumireObligatoriu dacă nu există id_produs / produs.
cantitateImplicit 1
pret_unitar / pretDacă lipsește și există produs, se ia prețul din catalog
cota_tva / tvaImplicit 21% (0% dacă firma e neplatitor de TVA)
discount_procent / discount_pct
discount_valoric / discount_val
creeaza_produstrue → salvează în catalog (sau găsește după cod_produs)
produsObiect cu denumire, cod_produs, tip, unitate_masura/um, pret_fara_tva, cota_tva
Pentru catalog separat: GET/POST /produse (scope products:read / products:write).

4.4 GET /facturi/{id}/pdf

Returnează PDF-ul (Content-Type: application/pdf).

4.5 POST /facturi/{id}/anaf

Trimite factura în SPV (e-Factura).

4.6 POST /facturi/{id}/incasare

Înregistrează o încasare pe factură.

{ "data": "2026-04-20", "suma": 238.00, "metoda": "card", "observatii": "" }

4.7 POST /facturi/{id}/storno

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

---

5. Proforme

CRUD similar cu facturile, fără integrare ANAF/scădere stoc.

---

6. Avize de însoțire a mărfii

Avizele NU se trimit în SPV (formularul e doar tipizat fiscal pe transport).

---

7. Clienți

---

8. Catalog produse

Scope: products:read / products:write

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 pe POST /facturi cu creeaza_produs / produs pe linie.

---

9. POS — case de marcat & bonuri fiscale

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.

9.0 Împerechere din aplicație (recomandat)

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.

  {
    "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

9.1 Case de marcat

9.2 Sesiuni de casă (deschidere/închidere zi de lucru)

  { "id_casa": 1, "sold_initial": 100.00, "observatii": "Schimb tura 1" }
  { "id_sesiune": 42, "sold_final": 1234.50 }
  // sau
  { "id_casa": 1 } // închide sesiunea deschisă curentă

9.3 Bonuri fiscale

  {
    "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." }
    }
  }
  { "motiv": "Greșeală operator" }

9.4 Rapoarte Z

  { "id_casa": 1, "id_sesiune": 42 }

---

10. Best practices client

(debounce buton de "Emite" 1500 ms) — server-ul nu are deduplicare automată.

---

10b. Preluare token ANAF (aplicații externe / WPF)

Autentificare: Authorization: Bearer {ANAF_PICKUP_API_SECRET} (din .env platformă). Nu folosește personal access token-urile mobile.

POST /anaf/pickup-link

Generează 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=RO12345678

Statusul ultimului link (fără a consuma tokenul). has_token: true când e gata.

GET /anaf/pickup-token?cui=RO12345678

One-shot: returnează access_token / refresh_token / expira_la și șterge rândul. 404 dacă nu e ready. Nu reîncercați după un 200.

---

11. Rate limiting

- GET /clienti/lookup-anaf → 30 cereri/minut/token

---

12. Smoke test rapid

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

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

---

13. Postman collection

Vezi docs/api-v1.postman_collection.json. Importă în Postman, setează variabilele de collection: baseUrl și token.