API pentru integrări

Versiunea v1 · Ultima actualizare: 12 noiembrie 2026 · URL de bază: https://facturare.completez.ro/api/v1 · specificație OpenAPI

API-ul permite unei aplicații externe — un magazin WooCommerce, un ERP sau un script propriu — să emită facturi și să le depună în SPV, fără să treacă prin interfața web. Un singur apel poate crea clientul, emite factura, genera XML-ul UBL 2.1 (CIUS-RO), îl valida la ANAF și îl încărca în SPV.

Specificația completă, în format OpenAPI 3.1: /api-docs/openapi.json. Poate fi importată direct în Postman sau Insomnia și folosită pentru a genera un client în limbajul dumneavoastră. Descrie toate cele 34 de operațiuni, schemele complete de cerere și răspuns, codurile de eroare și evenimentele webhook.

  1. Autentificare
  2. Mediul de test
  3. Codificare și diacritice
  4. Idempotență
  5. Erori și limite
  6. Endpoint-uri
  7. Stările depunerii
  8. Autorizarea ANAF
  9. Identificarea clientului
  10. Cote de TVA și prețuri
  11. Firme neplătitoare de TVA
  12. Facturi în valută
  13. Proforme
  14. Contracte și acordul GDPR
  15. Rulajul anual
  16. Răspunsul ANAF al facturii
  17. SPV: mesajele firmei
  18. Webhook-uri
  19. Administrarea cheilor
  20. Restricția pe adresă IP
  21. Depanare
  22. Exemplu complet: WooCommerce

Autentificare

Cheile se creează din aplicație, la Setări → API și integrări. O cheie este legată de o singură firmă și de drepturile alese la creare: read (citire) și opțional write (emitere și depunere).

Trimiteți cheia în oricare dintre cele două antete:

Authorization: Bearer efk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Key: efk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Numai server-to-server. Cheia dă drept de emitere a facturilor în numele firmei; nu o includeți în cod care ajunge în browser sau într-o aplicație mobilă. Din acest motiv rutele /api/v1 nu au CORS activat: un apel direct din pagina unui magazin va fi respins de browser.

Verificați rapid că totul e configurat corect:

curl https://facturare.completez.ro/api/v1/ping \
  -H "Authorization: Bearer efk_..."
{
  "ok": true,
  "versiune": "v1",
  "cheie": { "nume": "Magazin WooCommerce", "prefix": "efk_A1b2C3d4", "drepturi": ["read", "write"], "cheieDeTest": false },
  "firma": { "id": "68f…", "denumire": "SC EXEMPLU SRL", "cui": "12345678", "modTest": false },
  "mediu": "productie"
}

Pentru și mai multă siguranță, o cheie poate fi restrânsă la adresele IP ale serverului care o folosește: vezi Restricția pe adresă IP.

Mediul de test

ANAF are un mediu de test complet separat: aceleași apeluri, aceleași răspunsuri, fără efecte fiscale. Vă puteți dezvolta integrarea acolo fără să opriți facturarea reală.

Cheie de test (recomandat)

Bifați Cheie de test la crearea cheii. Tot ce emite acea cheie ajunge în mediul de test, indiferent dacă firma lucrează în producție, iar cheia vede și gestionează exclusiv facturi de test — listele, căutările și stornările ei nu ating documentele reale. Practic aveți două lumi paralele pe aceeași firmă: una pentru integrare, una pentru clienți.

În aplicație, cheile de test sunt marcate cu eticheta TEST, iar /ping vă spune mereu unde sunteți:

{ "cheie": { "cheieDeTest": true, … }, "mediu": "test" }

O singură factură în test

Cu o cheie obișnuită, puteți trimite "modTest": true la crearea unei facturi anume:

curl -X POST https://facturare.completez.ro/api/v1/invoices \
  -H "Authorization: Bearer efk_..." \
  -H "Content-Type: application/json" \
  -d '{ "modTest": true, "client": { "denumire": "Client de probă" },
        "linii": [{ "denumire": "Test", "cantitate": 1, "pretUnitar": 10 }] }'

Testul câștigă întotdeauna. Mediul e test dacă oricare dintre condiții e adevărată: cheia e de test, cererea cere test, sau firma e comutată pe test din aplicație. Câmpul "modTest": false nu poate readuce în producție o cheie de test — nu există combinație de setări care să trimită din greșeală o probă în SPV-ul real.

Ce trebuie știut

Codificare și diacritice

Totul este UTF-8: corpul cererilor, răspunsurile, XML-ul generat și fișierele trimise la ANAF. Nu trebuie să declarați nimic special — application/json este UTF-8 prin definiție — dar corpul trebuie chiar codificat așa.

Textul cu diacritice stricate este respins cu 400. Dacă integrarea trimite textul în CP1252 sau ISO-8859-2, diacriticele se pierd pe drum: „Probă” ajunge Prob�, iar „Bistrița-Năsăud” ajunge Bistri?a-N�s�ud. Acel � este caracterul de înlocuire U+FFFD, iar informația originală nu mai poate fi recuperată. Refuzăm cererea pe loc, fiindcă altfel ajunge pe o factură fiscală depusă la ANAF, de unde se corectează doar prin stornare.

În practică: asigurați-vă că serializarea JSON produce UTF-8. În PHP, wp_json_encode() și json_encode() fac asta implicit; problema apare de regulă când textul vine dintr-o bază de date cu altă colație și nu e convertit înainte. Semnul de întrebare simplu rămâne text legitim și este acceptat.

Idempotență

Trimiteți externalId la fiecare creare de factură — identificatorul comenzii din sistemul dumneavoastră (ex. wc_order_1234). Dacă apelul se repetă cu același externalId, nu se creează o a doua factură: primiți 200 cu "duplicat": true și factura deja existentă.

Asta face reîncercările sigure. Dacă rețeaua cade după ce am înregistrat factura, dar înainte să primiți răspunsul, pur și simplu reîncercați — veți primi factura originală, nu una nouă. Regula se aplică și apelurilor simultane: la o cursă între mai multe cereri cu același externalId, o singură factură este creată.

Erori și limite

Erorile vin ca JSON cu câmpul error, iar cele de validare au și un câmp details cu poziția exactă a problemei:

{
  "error": "Date invalide",
  "details": [{ "path": "linii.0.cantitate", "message": "Expected number, received string" }]
}
CodCând apare
400Payload invalid: lipsesc câmpuri, cotă de TVA necunoscută, dată în alt format decât YYYY-MM-DD
401Cheie lipsă, invalidă, revocată sau expirată
403Cheia are doar drept de citire, iar operațiunea cere write; sau cheia e restrânsă la anumite adrese IP, iar cererea vine de la alta (vezi Restricția pe adresă IP)
404Factura nu există sau aparține altei firme decât cea a cheii
402Limita planului gratuit a fost atinsă pentru luna curentă
409Firma nu e autorizată la ANAF, token expirat, factură deja depusă sau deja stornată, răspunsul ANAF al facturii încă nu e gata
422BNR n-are cursul cerut: monedă pe care n-o publică sau dată fără curs. Nu se creează nimic; reîncercarea n-ar ajuta
429Peste 240 de cereri pe minut pentru aceeași cheie, sau a doua sincronizare SPV a firmei în același minut. Antetul Retry-After spune peste câte secunde puteți relua
502ANAF nu a răspuns sau a răspuns cu eroare, la SPV sau la răspunsul unei facturi. Motivul e în error; reîncercați mai târziu
503BNR n-a putut fi întrebat (cu monedaPlata). Nu se creează nimic; reîncercați mai târziu

Limita de cereri se aplică pe cheie, nu pe adresă IP: mutarea magazinului pe alt server nu schimbă cota, iar două integrări găzduite la același furnizor nu se penalizează reciproc.

Endpoint-uri

POST/api/v1/invoices

Creează factura și, implicit, o depune în SPV. Clientul este identificat după externalId, apoi după CUI, apoi după denumire: dacă există deja ca partener al firmei, e refolosit; dacă nu, e creat (vezi Identificarea clientului). Când trimiteți doar CUI-ul unei firme românești, restul datelor sunt completate automat din registrul ANAF.

curl -X POST https://facturare.completez.ro/api/v1/invoices \
  -H "Authorization: Bearer efk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "wc_order_1234",
    "client": {
      "cui": "RO12345678",
      "denumire": "SC CLIENT SRL",
      "adresa": "Str. Exemplu nr. 10",
      "localitate": "Cluj-Napoca",
      "judet": "Cluj"
    },
    "linii": [
      { "denumire": "Abonament lunar", "cantitate": 1, "pretUnitar": 199.99, "cotaTva": "21" },
      { "denumire": "Transport", "cantitate": 1, "pretUnitar": 25, "cotaTva": "21" }
    ]
  }'
CâmpObligatoriuDescriere
clientdaCel puțin cui, denumire sau externalId. Restul: adresa, localitate, judet, codPostal, tara, codInregistrare, email, telefon, iban, banca, platitorTva
client.taranuCodul ISO 3166-1 alpha-2 al țării, în orice scriere (de sau DE). Implicit RO. Marea Britanie e GB, nu UK; Grecia, GR
client.codPostalnuCodul poștal; ajunge în XML (BT-53) și pe PDF
client.externalIdnu, dar vezi mai josIdentificatorul clientului la dumneavoastră (id pacient, id client). Obligatoriu în practică pentru persoane fizice — vezi Identificarea clientului
liniidaMinimum una, maximum 500. Fiecare: denumire, cantitate, pretUnitar, opțional cotaTva, um, umUbl
externalIdnu, dar recomandatIdentificatorul comenzii din sistemul dumneavoastră. Asigură idempotența
scutireTvanuPentru cota E: { "cod": "VATEX-EU-132-1C", "motiv": "art. 292 alin. (1) lit. a)" }. Vezi Cote de TVA
serianuImplicit seria marcată ca implicită în aplicație
dataEmiterenuYYYY-MM-DD. Implicit data de azi (ora României)
dataScadentanuYYYY-MM-DD
monedanuCodul ISO 4217, în orice scriere (usd devine USD). Implicit RON. Vezi Facturi în valută
cursValutarnuLei pentru o unitate din moneda (factură în valută) sau din monedaPlata. Lipsă = cursul BNR valabil la data facturii, luat de server
monedaPlatanuDoar cu moneda: "RON": moneda în care a plătit clientul (ex. USD). Prețurile liniilor sunt atunci în ea și se trec în lei. Vezi Factură în lei, plătită în valută
dataCursnuYYYY-MM-DD, ziua cursului trimis. Se trimite doar împreună cu cursValutar
modPlatanuImplicit transfer, care cere un IBAN al firmei. Pentru plățile cu cardul (Stripe), trimiteți card
observatiinuText liber, până la 2000 de caractere
depunenuImplicit true. Cu false, factura e doar creată; o depuneți ulterior

Numerele de document nu se trimit. Numărul facturii este alocat de server din seria firmei, iar totalurile sunt recalculate din linii. Valorile trimise pentru ele sunt ignorate — altfel o eroare din integrare ar ajunge într-un document fiscal depus la ANAF.

Răspuns 201:

{
  "duplicat": false,
  "depunere": {
    "status": "incarcata",
    "reusita": true,
    "indexIncarcare": "4761234567",
    "mesaj": "XML incarcat cu succes",
    "validare": { "valid": true, "mesaj": "" }
  },
  "factura": {
    "id": "68f1a2b3c4d5e6f7a8b9c0d1",
    "externalId": "wc_order_1234",
    "seria": "FCT", "numar": 128, "numarComplet": "FCT128",
    "dataEmitere": "2026-08-26",
    "client": { "denumire": "SC CLIENT SRL", "cui": "12345678", … },
    "linii": [ … ],
    "totalFaraTva": 224.99, "totalTva": 47.25, "total": 272.24,
    "depunere": { "status": "incarcata", "indexIncarcare": "4761234567", … },
    "anulata": false, "stornata": false, "modTest": false
  }
}

Factura se creează chiar dacă depunerea eșuează. Dacă firma nu e autorizată la ANAF sau token-ul a expirat, primiți tot 201, dar cu depunere.status egal cu eroare_depunere. Factura există și are număr alocat — ștergerea ei ar lăsa o gaură în numerotare. Reluați depunerea cu POST /invoices/:id/submit după ce ați rezolvat autorizarea.

GET/api/v1/invoices/:id

Returnează factura. Cu ?refresh=1 întrebăm ANAF pe loc care e starea curentă și v-o returnăm în câmpul anaf — alternativa la webhook-uri, pentru integrările care preferă să întrebe când au ele nevoie.

curl "https://facturare.completez.ro/api/v1/invoices/68f1a2b3c4d5e6f7a8b9c0d1?refresh=1" \
  -H "Authorization: Bearer efk_..."

GET/api/v1/invoices

Listă paginată. Parametri: status, externalId, from și to (YYYY-MM-DD, după data emiterii), limit (implicit 50, maximum 200) și offset.

curl "https://facturare.completez.ro/api/v1/invoices?status=nok&from=2026-08-01" \
  -H "Authorization: Bearer efk_..."

POST/api/v1/invoices/:id/submit

Depune o factură care nu a fost încă trimisă — după o eroare de depunere sau după o creare cu "depune": false. Dacă XML-ul lipsește, e generat acum — la fel când factura e în valută și nu avea încă cursul (fara_curs). O factură deja depusă întoarce 409.

POST/api/v1/invoices/:id/storno

Emite factura de stornare (valori negate) și o depune. Este operațiunea potrivită pentru o rambursare: o factură ajunsă la ANAF nu mai poate fi ștearsă sau modificată. Acceptă externalId (recomandat, ex. wc_refund_1234) și depune.

Stornoul păstrează moneda și cursul facturii anulate — și plata în valută a unei facturi în lei, cu aceeași mențiune —, deci în lei e exact opusul ei, iar XML-ul lui trimite la ea (numărul și data, în BillingReference). Reîncercarea cu același externalId întoarce 200 cu "duplicat": true și stornoul deja făcut, chiar dacă factura apare acum ca stornată. Un alt storno pentru o factură deja stornată, sau un externalId folosit de alt document, primește 409.

GET/api/v1/invoices/:id/xml

XML-ul UBL 2.1 exact așa cum a fost depus la ANAF. Dacă factura nu are încă XML (de pildă, fiindcă BNR nu a răspuns la creare), îl generează acum, fără să-l depună; dacă nici acum nu se poate, întoarce 409 cu motivul.

GET/api/v1/invoices/:id/pdf

PDF-ul facturii, randat de serviciul oficial ANAF din același XML — deci reprezintă exact documentul transmis. Cu ?model=modern: factura desenată de aplicație, documentul potrivit de dat clientului; la o firmă neplătitoare scrie „Neplătitor de TVA”, iar la o factură în valută are cursul și totalul în lei.

Cere autentificare, deci nu poate fi folosit ca link public către client: cheia ar ajunge în browserul lui. Descărcați-l pe server și serviți-l mai departe din aplicația dumneavoastră.

GET/api/v1/invoices/:id/raspuns

Arhiva ZIP pusă de ANAF în SPV pentru factura depusă: factura semnată de ANAF (la ok) sau erorile (la nok). Vezi Răspunsul ANAF al facturii.

GET/api/v1/ping

Verifică rapid cheia și spune în ce mediu va emite. Nu are niciun efect, deci poate fi apelat oricând, inclusiv dintr-o verificare de stare a integrării.

{
  "ok": true,
  "versiune": "v1",
  "cheie": { "nume": "Magazin WooCommerce", "prefix": "efk_A1b2C3d4",
             "drepturi": ["read", "write"], "cheieDeTest": false },
  "firma": { "id": "68f…", "denumire": "SC EXEMPLU SRL", "cui": "12345678", "modTest": false },
  "mediu": "productie",
  "server": "2026-09-19T08:14:22.031Z"
}

GET/api/v1/company

Datele firmei, starea autorizării ANAF, seriile disponibile și câte facturi mai puteți emite luna aceasta. Merită interogat înainte de prima comandă a zilei: dacă anaf.autorizat este false, depunerile vor eșua până când firma e reautorizată din aplicație.

{
  "denumire": "SC EXEMPLU SRL", "cui": "12345678", "modTest": false,
  "anaf": { "autorizat": true, "expirat": false, "expiraLa": "2026-09-25T10:00:00.000Z" },
  "serii": [{ "serie": "FCT", "numarCurent": 128, "implicita": true }],
  "abonament": { "plan": "anual", "nelimitat": true, "facturiRamaseLunaAceasta": null }
}

GET/api/v1/company/rulaj

Cifra de afaceri a firmei într-un an, în lei, lună cu lună — pentru pragurile fiscale. Vezi Rulajul anual.

SPV

Mesajele firmei din SPV — facturi primite și trimise, erori, mesaje de la cumpărători — prin /api/v1/spv/…: sincronizare, listă, descărcare și fișiere. Vezi SPV: mesajele firmei.

Stările depunerii

StareÎnseamnă
nullFactura există local, nu a fost trimisă
nedepusaCreată cu "depune": false
eroare_xmlXML-ul UBL nu s-a putut genera (date incomplete la firmă sau la client)
fara_cursDoar în răspunsul la creare: factura e în valută, iar cursul BNR nu a putut fi aflat. Factura există, fără XML, nedepusă; vezi Facturi în valută
eroare_validareANAF a respins factura la validare, înainte de încărcare
eroare_depunereFirma nu e autorizată, token expirat sau ANAF indisponibil
incarcataTrimisă la ANAF, în prelucrare. Are indexIncarcare
okANAF a validat factura. Stare finală
nokANAF a respins factura. Stare finală

Trecerea de la incarcata la ok sau nok durează de regulă câteva minute și nu depinde de dumneavoastră. O aflați dintr-un webhook sau interogând cu ?refresh=1.

Autorizarea ANAF

Depunerea în SPV se face cu autorizarea firmei la ANAF, obținută o singură dată din aplicație, cu certificatul digital (Autorizare). Integrarea nu participă la acest pas și nu are nevoie de certificat.

Autorizarea are o durată limitată, dar se reînnoiește automat: înainte de expirare, cât timp firma are token de reînnoire. Reînnoirea se face atât la folosire, cât și printr-o verificare de fundal care rulează chiar dacă nimeni nu deschide aplicația. Practic, integrarea nu ar trebui să întâlnească niciodată o autorizare expirată.

Dacă totuși reînnoirea eșuează — token de reînnoire absent sau revocat la ANAF — operațiunile de depunere întorc 409 cu un mesaj explicit, iar în GET /company veți vedea anaf.autorizat: false. Singura soluție în acel punct este o autorizare nouă, cu certificatul digital, din aplicație.

Interogați /company periodic (o dată pe zi e suficient) și avertizați dacă anaf.autorizat devine false. E singura situație în care facturile se opresc din motive care nu țin de datele trimise de dumneavoastră.

Identificarea clientului

Partenerul este căutat în evidența firmei, în această ordine:

  1. client.externalId — identificatorul lui la dumneavoastră;
  2. client.cui, normalizat (fără prefixul RO la clienții din România);
  3. client.denumire, potrivire exactă.

Adresa, localitatea și județul sunt obligatorii pentru clienții din România — inclusiv pentru persoane fizice. ANAF respinge facturile fără adresa completă a cumpărătorului (CIUS-RO cere județul ca element distinct), așa că API-ul refuză cererea cu 400 înainte de a aloca un număr de factură. Dacă trimiteți doar CUI-ul unei firme, restul datelor sunt completate din registrul ANAF.

Pentru persoane fizice, trimiteți client.externalId. Fără CUI, singura potrivire rămasă este numele — iar doi clienți numiți la fel ar fi contopiți în același partener, cu facturile amestecate. Un identificator stabil (id-ul pacientului, al abonatului, al contului) îi ține distincți și face ca a doua factură a aceleiași persoane să refolosească partenerul existent, nu să creeze altul.

Când un partener existent, creat din interfață, apare prima dată printr-un apel cu externalId, identificatorul îi este atașat automat — următoarele facturi îl regăsesc direct. Excepție: un apel din mediul de test nu atașează nimic unui client de producție.

Clientul găsit după externalId se actualizează

Un apel care descrie clientul (nu doar îi trimite externalId-ul) îi actualizează fișa, ca factura să poarte datele de acum ale clientului: numele, adresa, țara, codul poștal, e-mailul și codurile (cui, codInregistrare). Adresa, codul poștal, e-mailul și codurile se iau așa cum vin și se golesc dacă lipsesc din apel: un abonat mutat din Cluj în Germania nu păstrează județul, iar unul care a renunțat la firmă nu păstrează CUI-ul ei. Numele, țara și telefonul se schimbă doar când sunt trimise. Un apel doar cu externalId lasă fișa neatinsă.

Clientul găsit după CUI sau după denumire rămâne neatins. Un apel de test care dă peste un client de producție primește o copie de test a lui, cu datele din apel; fișa de producție nu se schimbă.

Clienți din afara României

Trimiteți client.tara (cod ISO, ex. DE, US). Codul de TVA al unei firme străine se păstrează întreg (DE123456789) și nu se caută în registrul ANAF, care are doar firme românești. Codul de județ și sectoarele Bucureștiului se aplică doar clienților din România, deci judet poate fi statul sau regiunea (TX, BC). Adresa completă nu e obligatorie pentru ei, dar ajunge pe factură așa cum o trimiteți.

Cote de TVA și prețuri

Câmpul cotaTva se trimite ca text sau ca număr. Valori standard: 21, 19, 11, 9, 5, 0. Coduri speciale: E (scutit) și AE (taxare inversă). Se acceptă și o cotă numerică personalizată, definită în aplicație.

pretUnitar este prețul FĂRĂ TVA. TVA-ul se adaugă peste, nu se extrage din el: pretUnitar: 700 cu cotaTva: "21" dă un total de 847 lei. Dacă prețul afișat clientului este final (cu TVA inclus), împărțiți înainte de a trimite: 700 / 1.21 = 578.51. La cotele 0, E și AE distincția nu contează — dar contează în ziua în care firma devine plătitoare de TVA, iar facturile ar începe brusc să iasă cu 21% peste prețul din vitrină.

Cota 0 rămâne 0. O cotă necunoscută nu este „reparată” la valoarea implicită — cererea este respinsă cu 400, fiindcă o factură cu TVA calculat greșit ajunge la ANAF și trebuie apoi stornată.

Temeiul scutirii (cota E)

Pentru facturile scutite, trimiteți temeiul real al scutirii. Fără el, XML-ul poartă formularea generică VATEX-EU-79-C / „Scutit de TVA”, care descrie altceva decât, de pildă, o scutire pentru servicii medicale.

{
  "linii": [{ "denumire": "Consultație", "cantitate": 1, "pretUnitar": 700, "cotaTva": "E" }],
  "scutireTva": {
    "cod": "VATEX-EU-132-1C",
    "motiv": "Scutit conform art. 292 alin. (1) lit. a) Cod fiscal"
  }
}

Codul se alege din lista VATEX a Comisiei Europene, iar motivul este textul care apare pe factură. O factură de stornare moștenește automat temeiul documentului pe care îl anulează.

Firme neplătitoare de TVA

Dacă firma e setată în aplicație ca neplătitoare de TVA, factura nu poate purta TVA, indiferent ce trimiteți: fiecare linie primește categoria O („neimpozabil”), fără cotă și cu TVA zero, iar cotaTva se ignoră. Totalul de plată e totalul liniilor. Regula se aplică pe server, la facturile din API și din interfață, la proforme și la contracte.

XML-ul urmează EN 16931 pentru categoria O:

PDF-ul scrie „Neplătitor de TVA” și nu are coloane de TVA. O firmă plătitoare care trimite cota O primește 400.

Facturi în valută

Trimiteți moneda (ex. USD, EUR). Factura rămâne în valută, dar ANAF cere TVA-ul și în lei (BR-RO-030), deci factura primește un curs:

Răspunsul arată cursValutar, dataCurs și sursaCurs (bnr sau apelant). XML-ul are TaxCurrencyCode RON, TVA-ul în lei și o notă cu cursul; PDF-ul are același rând și totalul în lei:

Curs BNR din 30.10.2026: 1 USD = 4,7698 lei; total 15,00 USD = 71,55 lei

Dacă BNR nu răspunde, factura se creează oricum (201), cu depunere.status: "fara_curs" și motivul în depunere.mesaj: nu are încă XML și nu s-a depus. Reluați mai târziu cu POST /invoices/:id/submit, sau cereți GET /invoices/:id/xml, care generează XML-ul fără să-l depună. Dacă însă BNR a răspuns, dar n-are cursul (o monedă pe care n-o publică, o dată fără curs), factura nu se creează: 422.

Un abonament plătit cu cardul, de un client din Germania, fără depunere în SPV:

{
  "externalId": "in_1QabcD",
  "moneda": "usd",
  "modPlata": "card",
  "depune": false,
  "client": {
    "externalId": "cus_R2xyz",
    "denumire": "Beispiel Software GmbH",
    "cui": "DE123456789",
    "tara": "DE",
    "adresa": "Friedrichstraße 100",
    "localitate": "Berlin",
    "codPostal": "10117"
  },
  "linii": [
    { "denumire": "Abonament Business - lunar", "cantitate": 1, "pretUnitar": 49.9, "um": "luna", "umUbl": "MON" }
  ]
}

Factură în lei, plătită în valută

Când clientul a plătit în valută, dar factura trebuie să fie în lei (clienții români, a căror contabilitate cere lei), trimiteți moneda: "RON" și monedaPlata (ex. USD), cu prețurile liniilor în moneda plății. Serverul le trece în lei la cursul BNR valabil la data facturii, sau la cursValutar, care e atunci cursul monedei plății:

Plătit 15,00 USD; curs BNR din 30.10.2026: 1 USD = 4,7698 lei.

Răspunsul are totalul în lei în total și, în plus, monedaPlata, cursPlata (lei pentru o unitate), dataCursPlata, sursaCursPlata (bnr sau apelant) și totalPlata (suma plătită, în valută). Fără curs nu se creează nimic: dacă BNR nu răspunde, primiți 503 și puteți reîncerca, fără număr consumat; dacă BNR n-are cursul (monedă pe care n-o publică, dată fără curs), primiți 422, iar reîncercarea n-ar ajuta. În aplicație, unei asemenea facturi nu i se mai pot schimba data sau moneda: se emite alta. monedaPlata merge doar cu moneda: "RON"; lipsă sau egală cu moneda, cererea e exact ca înainte.

{
  "externalId": "in_1QefgH",
  "moneda": "RON",
  "monedaPlata": "USD",
  "modPlata": "card",
  "client": {
    "externalId": "cus_R3abc",
    "denumire": "SC CLIENT SRL",
    "cui": "RO12345678",
    "tara": "RO",
    "adresa": "Str. Lăpușneanu 12",
    "localitate": "Iași",
    "judet": "Iași",
    "codPostal": "700057"
  },
  "linii": [
    { "denumire": "Abonament Pro - lunar", "cantitate": 1, "pretUnitar": 29, "um": "luna", "umUbl": "MON" }
  ]
}

Proforme

Proforma nu este document fiscal: nu se depune la ANAF, nu are XML și nu are stare de depunere. Rolul ei e să preceadă plata. Operațiunea care contează este conversia în factură — factura rezultată urmează apoi exact drumul obișnuit către SPV.

OperațiuneCe face
POST /proformasCreează, cu client inline ca la facturi
GET /proformasListă, cu filtre externalId, anulata, facturata, from, to
GET /proformas/:idReturnează o proformă
PUT /proformas/:idModifică; se schimbă doar câmpurile trimise
POST /proformas/:id/anulareAnulează, cu motiv opțional
POST /proformas/:id/to-invoiceConvertește în factură și o depune

Proformele au seria lor (Setări → Serii, secțiunea Proforme), separată de cea a facturilor. Restul se comportă identic: externalId pentru idempotență, aceleași reguli pentru client, aceleași cote de TVA, aceeași izolare între mediul de test și producție.

Conversia în factură

curl -X POST https://facturare.completez.ro/api/v1/proformas/68f…/to-invoice \
  -H "Authorization: Bearer efk_..." \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "wc_order_1234" }'

Factura preia clientul, liniile și totalurile proformei, primește număr din seria de facturi și este depusă imediat, dacă nu trimiteți "depune": false. Răspunsul conține factura, starea depunerii și proforma actualizată.

Mediul se moștenește. O proformă de test produce întotdeauna o factură de test — nu poate consuma un număr din seria reală și cu atât mai puțin ajunge în SPV-ul de producție.

Conversia se face o singură dată. Legătura către factură se scrie înainte de depunere, deci chiar dacă depunerea eșuează, proforma rămâne marcată drept convertită și o a doua încercare întoarce 409, în loc să emită încă o factură. Depunerea se reia apoi cu POST /invoices/:id/submit pe factura deja creată.

După conversie, proforma nu mai poate fi modificată sau anulată: documentul emis și cel care l-a justificat ar ajunge să spună lucruri diferite. Corectarea se face pe factură, iar dacă a ajuns deja la ANAF, prin stornare.

Contracte și acordul GDPR

Contractul nu se depune nicăieri — rolul lui e să stea la baza facturilor, care se emit din el printr-o operațiune explicită. Documentele (contractul și acordul de prelucrare a datelor) se generează la cerere, în format Word.

OperațiuneCe face
POST /contractsCreează, cu client inline ca la facturi
GET /contractsListă, cu filtre stare, externalId, from, to
GET /contracts/:idReturnează un contract
PUT /contracts/:idModifică; se schimbă doar câmpurile trimise
POST /contracts/:id/anulareAnulează, cu motiv opțional
GET /contracts/:id/docxContractul ca document Word
GET /contracts/:id/dpaAcordul GDPR ca document Word
POST /contracts/:id/emite-facturaEmite o factură în baza contractului

Seria contractelor e text liber (implicit CTR) și nu cere configurare în aplicație, spre deosebire de seriile de facturi și proforme. Obiectul contractului e obligatoriu și apare ca atare în capitolul II al documentului.

Emiterea facturii

curl -X POST https://facturare.completez.ro/api/v1/contracts/68f…/emite-factura \
  -H "Authorization: Bearer efk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "ctr12_2026_10",
    "linii": [{ "denumire": "Abonament — octombrie 2026", "cantitate": 1, "pretUnitar": 500, "cotaTva": "21" }]
  }'

Fără linii, se folosesc pozițiile din contract. Scadența se calculează din termenPlataZile, factura moștenește mediul contractului și rămâne legată de el — numarFacturiEmise și lista facturi se actualizează.

Emiterea e manuală, intenționat. Câmpurile frecventaFacturare și ziFacturare descriu înțelegerea părților și apar în contract, dar nu declanșează nimic singure. Când facturarea va fi pusă pe automat, un job va apela exact această operațiune — deci comportamentul rămâne unul singur, deja verificat.

Acordul de prelucrare a datelor (art. 28 GDPR)

Se activează cu dpa.activ și devine anexă la contract. Documentul cuprinde cele 11 articole, Anexa 1 cu descrierea prelucrării și Anexa 2 cu măsurile tehnice și organizatorice.

{
  "dpa": {
    "activ": true,
    "anexaNr": 2,
    "rolFirma": "imputernicit",
    "obiectPrelucrare": "Furnizarea, mentenanța și suportul aplicației.",
    "persoaneVizate": "Angajați și colaboratori ai Operatorului.",
    "categoriiDate": "Date de identificare și de contact.",
    "categoriiSpeciale": "Nu se prelucrează categorii speciale de date.",
    "subimputerniciti": [
      { "denumire": "Hetzner Online GmbH", "rol": "Găzduire", "locatie": "Germania (UE)" }
    ],
    "dpoNume": "…", "dpoContact": "…"
  }
}

rolFirma schimbă textul, nu doar denumirile. Când prestați un serviciu care atinge datele clientului — găzduire, mentenanță, contabilitate — firma dumneavoastră e împuternicit, iar clientul operator. Invers, dacă clientul prelucrează pentru dumneavoastră, alegeți operator. Obligațiile din articolele 3 și 4 se atribuie în consecință.

Câmpurile lăsate goale primesc formulări implicite rezonabile pentru un contract de prestări servicii. Dacă prelucrați categorii speciale de date (art. 9 GDPR — sănătate, date biometrice), completați categoriiSpeciale: apare explicit în Anexa 1, iar acordul cere garanții suplimentare.

Documentele ies în format Word, nu PDF, tocmai pentru că părțile le mai ajustează înainte de semnare. Se generează la cerere din datele curente, deci nu pot rămâne în urmă față de contract.

Rulajul anual

Cifra de afaceri a firmei într-un an calendaristic, în lei — pentru pragurile fiscale pe care le urmărește integrarea (plafonul de TVA, cel de microîntreprindere).

curl "https://facturare.completez.ro/api/v1/company/rulaj?an=2026" \
  -H "Authorization: Bearer efk_..."
{
  "an": 2026,
  "moneda": "RON",
  "mediu": "productie",
  "facturi": 12,
  "totalFaraTva": 12345.67,
  "totalTva": 2592.59,
  "total": 14938.26,
  "faraCurs": 0,
  "peLuni": [
    { "luna": "2026-01", "facturi": 3, "totalFaraTva": 2100, "totalTva": 441, "total": 2541 },
    …
  ],
  "calculatLa": "2026-11-12T10:00:00.000Z"
}

Mediul îl dă cheia. O cheie de producție numără doar facturile reale — chiar dacă firma e comutată pe test din aplicație —, iar una de test doar facturile de test. Probele nu sunt cifră de afaceri.

Răspunsul ANAF al facturii

După prelucrare, ANAF pune în SPV o arhivă ZIP pentru fiecare factură depusă: factura semnată de ANAF când starea e ok, erorile când e nok. Factura semnată e dovada transmiterii, iar erorile spun ce trebuie corectat.

curl -o FCT128_raspuns_ANAF.zip \
  "https://facturare.completez.ro/api/v1/invoices/68f1a2b3c4d5e6f7a8b9c0d1/raspuns" \
  -H "Authorization: Bearer efk_..."

Prima cerere întreabă ANAF starea facturii (ca ?refresh=1: depunere.status se actualizează și pe factură), descarcă arhiva și o păstrează; următoarele o servesc direct, fără ANAF. Răspunsul are Content-Type: application/zip și numele <SERIE>_<NUMAR>_raspuns_ANAF.zip.

CodCând
200Arhiva ZIP
409„Factura nu a fost depusă la ANAF”; „ANAF încă prelucrează factura” (reîncercați peste câteva minute); un răspuns pe care ANAF nu l-a dat (de pildă „XML cu erori nepreluat de sistem”); firma nu e autorizată la ANAF
502ANAF nu a răspuns sau a răspuns cu eroare; reîncercați mai târziu

ANAF permite doar 10 descărcări pe zi pentru același mesaj. De aceea arhiva se descarcă o singură dată și se păstrează: o puteți cere de câte ori aveți nevoie, fără să consumați din limită.

SPV: mesajele firmei

Mesajele firmei din Spațiul Privat Virtual — facturile primite de la furnizori, cele trimise, erorile și mesajele de la cumpărători — se aduc de la ANAF printr-o sincronizare, se păstrează pe server și se citesc apoi fără apeluri la ANAF. Sunt aceleași mesaje pe care le vedeți în aplicație, la Primite, Depuse și Erori.

OperațiuneCe face
POST /spv/sincronizareAduce de la ANAF mesajele din ultimele zile zile (1–60, implicit 60)
GET /spv/mesajeLista, cu filtre tip, from, to, cautare, limit, offset
GET /spv/mesaje/:idUn mesaj
POST /spv/mesaje/:id/descarcareDescarcă arhiva, citește factura sau eroarea, face PDF-ul
GET /spv/mesaje/:id/pdf, /xml, /zipFișierele mesajului; îl descarcă întâi, dacă e nevoie
POST /spv/descarcareDescarcă în lot mesajele încă nedescărcate

Sincronizarea

curl -X POST https://facturare.completez.ro/api/v1/spv/sincronizare \
  -H "Authorization: Bearer efk_..." \
  -H "Content-Type: application/json" \
  -d '{ "zile": 60 }'
{
  "total": 37,
  "noi": 5,
  "de": "2026-09-13T10:01:00.000Z",
  "pana": "2026-11-12T09:59:55.000Z",
  "mediu": "productie",
  "sincronizatLa": "2026-11-12T10:00:02.418Z"
}

Sincronizarea cere de la ANAF toate tipurile de mesaje, pagină cu pagină, și le salvează pe cele noi; mesajele deja aduse — prin API sau din aplicație — nu se dublează. total e numărul de mesaje din interval, noi câte nu erau încă salvate. Se poate face cel mult o dată pe minut pentru o firmă: mai des primiți 429, cu antetul Retry-After. ANAF dă mesajele din cel mult ultimele 60 de zile, deci sincronizați măcar o dată la câteva săptămâni, ca să nu pierdeți niciunul.

Lista și un mesaj

curl "https://facturare.completez.ro/api/v1/spv/mesaje?tip=primite&from=2026-11-01&cautare=furnizor" \
  -H "Authorization: Bearer efk_..."

Lista vine cu cele mai noi mesaje primele, ca { "total", "limit", "offset", "sincronizatLa", "mesaje": [ … ] }, unde sincronizatLa e ultima sincronizare prin API (null dacă n-a fost niciuna). Filtrul tip acceptă primite, trimise, erori și mesaje; from și to sunt zile AAAA-LL-ZZ din calendarul României, cu capetele incluse; cautare (cel mult 100 de caractere) caută textul, literal, în emitent, beneficiar, numărul facturii, CIF-uri și detalii. GET /spv/mesaje/:id dă un singur mesaj:

{
  "id": "6911c0ffee0000000000abcd",
  "tip": "primita",
  "tipAnaf": "FACTURA PRIMITA",
  "idDescarcare": "3001293434",
  "indexIncarcare": "5001120362",
  "dataCreare": "2026-11-12T08:30:00.000Z",
  "detalii": "Factura cu id_incarcare=5001120362 emisa de cif_emitent=123 pentru cif_beneficiar=456",
  "cifEmitent": "123", "cifBeneficiar": "456",
  "emitent": "FURNIZOR SRL", "beneficiar": "SC EXEMPLU SRL",
  "numarFactura": "ABC 123", "dataFactura": "2026-11-10", "valoare": 1210.5,
  "descarcat": true,
  "fisiere": { "pdf": true, "xml": true, "zip": true },
  "mesajEroare": null
}
CâmpCe e
tipprimita, trimisa, eroare, mesaj (de la cumpărător) sau altul; tipul exact dat de ANAF e în tipAnaf
idDescarcare, indexIncarcareid și id_solicitare din lista ANAF
dataCreareMomentul mesajului, ca dată UTC. ANAF îl scrie în ora României (202611121030); conversia ține cont de ora de vară
cifEmitent, cifBeneficiarFără prefixul RO: din factura descărcată sau, până atunci, din detalii
emitent, beneficiar, numarFactura, dataFactura, valoareCitite din factură, după descărcare; null până atunci. valoare e totalul de plată, în moneda facturii
mesajEroareLa mesajele de eroare descărcate: erorile date de ANAF, câte una pe rând

Descărcarea și fișierele

POST /spv/mesaje/:id/descarcare aduce arhiva de la ANAF, o dezarhivează și citește factura (sau textul erorii); pentru facturi face și PDF-ul, cu serviciul de transformare ANAF. Răspunsul e mesajul, cu datele completate. GET /spv/mesaje/:id/pdf, /xml și /zip dau fișierele — dacă mesajul nu era descărcat, îl descarcă întâi. Mesajele de eroare și cele de la cumpărători nu au PDF (404); conținutul lor e în /xml.

Pentru mai multe mesaje deodată, POST /spv/descarcare cu { "tip": "primite", "limita": 20 } (ambele opționale; limita între 1 și 50, implicit 20) descarcă mesajele încă nedescărcate, cele mai noi primele, câte trei deodată:

{ "incercate": 20, "descarcate": 19, "ramase": 14,
  "erori": [{ "id": "6911…", "mesaj": "Descărcarea mesajului de la ANAF a eșuat: …" }] }

Repetați cât timp ramase e mai mare ca zero și descarcate nu e zero. Un mesaj care nu a putut fi descărcat stă deoparte 10 minute, ca loturile următoare să treacă la celelalte.

Fiecare arhivă se descarcă o singură dată. ANAF permite 10 descărcări pe zi pentru un mesaj, așa că arhiva se păstrează pe server după prima descărcare, iar PDF-ul, XML-ul și datele facturii se citesc de acolo. Cererile repetate nu mai ajung la ANAF.

Mediul și drepturile. O cheie de test, sau o firmă comutată pe test, citește SPV-ul de test al ANAF și vede doar mesajele aduse de acolo; o cheie de producție nu le vede pe acestea. Sincronizarea și descărcările nu schimbă nimic fiscal, deci merg și cu o cheie doar de citire. O firmă neautorizată la ANAF primește 409; o eroare de la ANAF, 502, cu motivul în error.

Webhook-uri

Setați un URL pe cheie (la creare sau ulterior, din aplicație) și vă anunțăm când se schimbă starea unei facturi depuse prin API. Livrarea se reîncearcă în backoff exponențial — 30 de secunde, 2, 10 și 30 de minute, apoi 2, 6 și 12 ore — deci un magazin oprit temporar nu pierde evenimentul.

Evenimente

EvenimentCând
invoice.submittedFactura a fost încărcată la ANAF
invoice.submit_failedÎncărcarea a eșuat
invoice.validatedANAF a validat factura
invoice.rejectedANAF a respins factura

Corpul cererii

POST https://magazinul-meu.ro/wc-api/efact
Content-Type: application/json
X-Efact-Event: invoice.validated
X-Efact-Timestamp: 1787654321
X-Efact-Signature: sha256=3f6c…
X-Efact-Delivery: 68f1a2b3c4d5e6f7a8b9c0d2

{
  "event": "invoice.validated",
  "invoiceId": "68f1a2b3c4d5e6f7a8b9c0d1",
  "externalId": "wc_order_1234",
  "seria": "FCT",
  "numar": 128,
  "indexIncarcare": "4761234567",
  "status": "ok",
  "downloadId": "4769876543"
}

Verificarea semnăturii

Semnătura este HMAC-SHA256 peste timestamp + "." + corpul brut, cu secretul afișat la crearea cheii. Verificați-o întotdeauna: fără ea, oricine cunoaște URL-ul poate trimite evenimente false. Timestamp-ul face ca o notificare interceptată să nu poată fi retrimisă mai târziu.

<?php
$payload   = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_EFACT_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_EFACT_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $payload, EFACT_WEBHOOK_SECRET);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
// Respingem notificările mai vechi de 5 minute (protecție împotriva retrimiterii).
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit;
}

$event = json_decode($payload, true);
// … actualizați comanda după $event['externalId'] și $event['status']

http_response_code(200);

Răspundeți cu un cod 2xx. Orice altceva — sau lipsa unui răspuns în 10 secunde — programează o reîncercare. Redirectările nu sunt urmate: notificarea merge exact la adresa configurată.

Adresa trebuie să fie publică. Înainte de fiecare livrare rezolvăm gazda și refuzăm să trimitem dacă ajunge într-un interval intern — loopback, rețele private, link-local. Verificarea e pe adresa rezolvată, nu pe cum arată numele, deci nici un domeniu public care indică 10.0.0.5 nu trece. Dacă webhook-ul dumneavoastră stă într-o rețea privată, expuneți-l printr-un domeniu public sau printr-un tunel.

Administrarea cheilor

Cheile se creează și se revocă din aplicație, la Setări → API și integrări. Tot acolo se schimbă URL-ul webhook-ului și se poate vedea jurnalul livrărilor.

Ce puteți faceUnde
Creare cheie (nume, firmă, drepturi, cheie de test, expirare, webhook, adrese IP permise)Setări → API și integrări
Adresele IP permise; ultima adresă folosită și ultima refuzatăbutonul Editează și Adaugă această adresă, lângă cheie
Revocare imediatăbutonul Revocă din lista de chei
Ștergere definitivă, cu tot cu istoricul livrărilorbutonul Șterge
Schimbarea URL-ului webhookbutonul Webhook
Regenerarea secretului webhookdin aceeași pagină
Jurnalul ultimelor livrări (stare, încercări, eroare)vezi livrările, lângă cheie

Cheia se vede o singură dată, la creare, împreună cu secretul webhook. În baza de date păstrăm doar amprenta ei, deci nu o putem reafișa — dacă se pierde, creați alta și revocați-o pe cea veche. Fiecare cont poate avea cel mult 20 de chei active.

Aceste operațiuni folosesc sesiunea din aplicație, nu cheia de API: o cheie nu poate crea sau modifica alte chei. Este intenționat — altfel o integrare compromisă și-ar putea prelungi singură accesul.

Restricția pe adresă IP

O cheie poate fi legată de adresele IP ale serverului care o folosește. O cheie scăpată — într-un commit, într-o copie de siguranță, în jurnalul unui furnizor — nu mai poate fi folosită atunci de pe alt calculator. Lista se pune din aplicație, la crearea cheii sau cu Editează: câte o adresă pe rând, IPv4 sau IPv6, ori intervale CIDR, cel mult 20.

18.199.208.221
10.0.0.0/8
2a05:d014::/32

Fără listă, cheia merge de oriunde, ca până acum. O cerere de la altă adresă primește 403, nu 401 — cheia e bună, doar locul de unde e folosită nu e —, cu adresa văzută de server în mesaj:

{ "error": "Cheia de API nu poate fi folosită de la adresa 198.51.100.7. Adăugați adresa la cheie, în Integrări." }

Adresa trebuie să fie fixă. Dacă serverul integrării iese pe internet prin adrese care se schimbă (unele găzduiri partajate, funcțiile serverless), puneți intervalul furnizorului sau lăsați lista goală: altfel cererile vor fi refuzate la prima schimbare.

Depanare

Factura nu ajunge la ANAF

Verificați depunere.status din răspuns. Fiecare stare de eroare are o cauză distinctă, descrisă la Stările depunerii; câmpul depunere.mesaj conține răspunsul ANAF, inclusiv regula încălcată (ex. BR-RO-110 pentru județul cumpărătorului lipsă).

Jurnalul complet este în aplicație. Pe pagina fiecărei facturi, secțiunea Comunicare cu SPV arată fiecare validare, încărcare și verificare de stare: rezultatul, durata, mediul, cine a inițiat-o (interfață, API sau verificare automată) și răspunsul brut al ANAF. Acolo se vede motivul exact al unei respingeri, inclusiv pentru facturile emise prin API.

Webhook-ul nu ajunge

În aplicație, lângă cheie, vezi livrările arată ultimele notificări cu starea lor: livrată, în așteptare sau abandonată, cu numărul de încercări și ultima eroare. Cauzele frecvente sunt un răspuns diferit de 2xx, un răspuns mai lent de 10 secunde, o redirectare (nu sunt urmate) sau o adresă care rezolvă într-o rețea privată (refuzată din principiu).

Factura a fost creată, dar cu date greșite

Cât timp nu a ajuns la ANAF — adică nu are depunere.indexIncarcare — poate fi corectată din aplicație și redepusă; editarea invalidează XML-ul vechi, astfel încât redepunerea pornește de la datele noi. După ce a ajuns la ANAF, singura cale este stornarea.

Exemplu complet: WooCommerce

Codul de mai jos emite factura la finalizarea comenzii și o stornează la rambursare. Se pune în functions.php al temei-copil sau într-un plugin propriu.

<?php
define('EFACT_API', 'https://facturare.completez.ro/api/v1');
define('EFACT_KEY', 'efk_...');           // din Setări → API și integrări
define('EFACT_COTA_TVA', '21');

function efact_post($cale, $date) {
    $res = wp_remote_post(EFACT_API . $cale, [
        'timeout' => 45,                  // depunerea la ANAF poate dura
        'headers' => [
            'Authorization' => 'Bearer ' . EFACT_KEY,
            'Content-Type'  => 'application/json',
        ],
        'body'    => wp_json_encode($date),
    ]);
    if (is_wp_error($res)) return null;
    return json_decode(wp_remote_retrieve_body($res), true);
}

add_action('woocommerce_order_status_completed', function ($order_id) {
    $order = wc_get_order($order_id);
    if (!$order || $order->get_meta('_efact_invoice_id')) return;   // deja facturată

    $linii = [];
    foreach ($order->get_items() as $item) {
        $cantitate = max((float) $item->get_quantity(), 1);
        $linii[] = [
            'denumire'   => $item->get_name(),
            'cantitate'  => $cantitate,
            'pretUnitar' => round((float) $item->get_subtotal() / $cantitate, 2),
            'cotaTva'    => EFACT_COTA_TVA,
        ];
    }
    if ((float) $order->get_shipping_total() > 0) {
        $linii[] = [
            'denumire'   => 'Transport',
            'cantitate'  => 1,
            'pretUnitar' => round((float) $order->get_shipping_total(), 2),
            'cotaTva'    => EFACT_COTA_TVA,
        ];
    }

    $raspuns = efact_post('/invoices', [
        'externalId' => 'wc_order_' . $order_id,
        'client' => [
            'cui'        => $order->get_meta('_billing_cui'),        // câmp propriu, dacă îl colectați
            'denumire'   => $order->get_billing_company()
                            ?: trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()),
            'adresa'     => trim($order->get_billing_address_1() . ' ' . $order->get_billing_address_2()),
            'localitate' => $order->get_billing_city(),
            'judet'      => $order->get_billing_state(),
            'email'      => $order->get_billing_email(),
            'telefon'    => $order->get_billing_phone(),
        ],
        'linii'       => $linii,
        'observatii'  => 'Comanda #' . $order->get_order_number(),
        'depune'      => true,
    ]);

    if (empty($raspuns['factura']['id'])) {
        $order->add_order_note('e-Factura: emiterea a eșuat.');
        return;
    }

    $order->update_meta_data('_efact_invoice_id', $raspuns['factura']['id']);
    $order->save();
    $order->add_order_note(sprintf(
        'e-Factura %s emisă. Depunere: %s',
        $raspuns['factura']['numarComplet'],
        $raspuns['depunere']['status'] ?? 'necunoscută'
    ));
});

add_action('woocommerce_order_refunded', function ($order_id, $refund_id) {
    $order      = wc_get_order($order_id);
    $invoice_id = $order->get_meta('_efact_invoice_id');
    if (!$invoice_id) return;

    $raspuns = efact_post('/invoices/' . $invoice_id . '/storno', [
        'externalId' => 'wc_refund_' . $refund_id,
    ]);

    if (!empty($raspuns['factura']['numarComplet'])) {
        $order->add_order_note('e-Factura: storno ' . $raspuns['factura']['numarComplet']);
    }
}, 10, 2);

Testați întâi în modul TEST. Comutați firma pe modul test din aplicație: facturile ajung în mediul de test ANAF, cu aceleași răspunsuri, fără efecte fiscale reale. Câmpul modTest din răspunsuri vă spune în ce mediu lucrați.

Suport

Întrebări despre integrare: contact@e-factura.app. Includeți prefixul cheii (efk_…, primele caractere) și externalId-ul comenzii — niciodată cheia completă.