API pentru integrări
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.
- Autentificare
- Mediul de test
- Codificare și diacritice
- Idempotență
- Erori și limite
- Endpoint-uri
- Stările depunerii
- Autorizarea ANAF
- Identificarea clientului
- Cote de TVA și prețuri
- Firme neplătitoare de TVA
- Facturi în valută
- Proforme
- Contracte și acordul GDPR
- Rulajul anual
- Răspunsul ANAF al facturii
- SPV: mesajele firmei
- Webhook-uri
- Administrarea cheilor
- Restricția pe adresă IP
- Depanare
- 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
- Autorizarea ANAF e aceeași. Nu aveți nevoie de un al doilea token: firma autorizată o dată funcționează în ambele medii.
-
Numerotarea e comună. Facturile de test consumă numere din aceeași
serie ca cele reale. Dacă vă deranjează golurile, definiți o serie separată (ex.
TEST) și trimiteți-o în câmpulseria. -
externalIde separat pe medii. Puteți folosiwc_order_1234și în test, și în producție — sunt facturi distincte, iar proba de test nu blochează comanda reală. -
Clienții sunt separați, dar într-o singură direcție. Un client creat
dintr-o factură de test rămâne în test și nu apare niciodată în producție. Invers,
clienții reali sunt vizibili din test și pot fi folosiți ca atare într-o probă — dar
nu sunt modificați de ea: dacă un apel de test găsește un client real după CUI,
identificatorul
client.externalIdtrimis atunci nu i se atașează. - Testele nu consumă cota planului. Facturile din mediul de test nu scad din limita lunară a planului gratuit — puteți proba integrarea de câte ori e nevoie.
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" }]
}
| Cod | Când apare |
|---|---|
400 | Payload invalid: lipsesc câmpuri, cotă de TVA necunoscută, dată în alt format decât YYYY-MM-DD |
401 | Cheie lipsă, invalidă, revocată sau expirată |
403 | Cheia 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) |
404 | Factura nu există sau aparține altei firme decât cea a cheii |
402 | Limita planului gratuit a fost atinsă pentru luna curentă |
409 | Firma nu e autorizată la ANAF, token expirat, factură deja depusă sau deja stornată, răspunsul ANAF al facturii încă nu e gata |
422 | BNR n-are cursul cerut: monedă pe care n-o publică sau dată fără curs. Nu se creează nimic; reîncercarea n-ar ajuta |
429 | Peste 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 |
502 | ANAF 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 |
503 | BNR 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âmp | Obligatoriu | Descriere |
|---|---|---|
client | da | Cel puțin cui, denumire sau externalId. Restul: adresa, localitate, judet, codPostal, tara, codInregistrare, email, telefon, iban, banca, platitorTva |
client.tara | nu | Codul ISO 3166-1 alpha-2 al țării, în orice scriere (de sau DE). Implicit RO. Marea Britanie e GB, nu UK; Grecia, GR |
client.codPostal | nu | Codul poștal; ajunge în XML (BT-53) și pe PDF |
client.externalId | nu, dar vezi mai jos | Identificatorul clientului la dumneavoastră (id pacient, id client). Obligatoriu în practică pentru persoane fizice — vezi Identificarea clientului |
linii | da | Minimum una, maximum 500. Fiecare: denumire, cantitate, pretUnitar, opțional cotaTva, um, umUbl |
externalId | nu, dar recomandat | Identificatorul comenzii din sistemul dumneavoastră. Asigură idempotența |
scutireTva | nu | Pentru cota E: { "cod": "VATEX-EU-132-1C", "motiv": "art. 292 alin. (1) lit. a)" }. Vezi Cote de TVA |
seria | nu | Implicit seria marcată ca implicită în aplicație |
dataEmitere | nu | YYYY-MM-DD. Implicit data de azi (ora României) |
dataScadenta | nu | YYYY-MM-DD |
moneda | nu | Codul ISO 4217, în orice scriere (usd devine USD). Implicit RON. Vezi Facturi în valută |
cursValutar | nu | Lei pentru o unitate din moneda (factură în valută) sau din monedaPlata. Lipsă = cursul BNR valabil la data facturii, luat de server |
monedaPlata | nu | Doar 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ă |
dataCurs | nu | YYYY-MM-DD, ziua cursului trimis. Se trimite doar împreună cu cursValutar |
modPlata | nu | Implicit transfer, care cere un IBAN al firmei. Pentru plățile cu cardul (Stripe), trimiteți card |
observatii | nu | Text liber, până la 2000 de caractere |
depune | nu | Implicit 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ă |
|---|---|
null | Factura există local, nu a fost trimisă |
nedepusa | Creată cu "depune": false |
eroare_xml | XML-ul UBL nu s-a putut genera (date incomplete la firmă sau la client) |
fara_curs | Doar î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_validare | ANAF a respins factura la validare, înainte de încărcare |
eroare_depunere | Firma nu e autorizată, token expirat sau ANAF indisponibil |
incarcata | Trimisă la ANAF, în prelucrare. Are indexIncarcare |
ok | ANAF a validat factura. Stare finală |
nok | ANAF 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:
client.externalId— identificatorul lui la dumneavoastră;client.cui, normalizat (fără prefixulROla clienții din România);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:
- liniile au categoria
O, fără procent; - o singură defalcare
O, cu TVA0și temeiulVATEX-EU-O/ „Neplătitor de TVA”; - fără codul de TVA al vânzătorului (BT-31) și fără cel al cumpărătorului (BT-48);
CUI-ul firmei apare ca identificator legal (BT-30), fără
RO; - clientul român apare cu CUI-ul ca identificator legal (BT-47); clientul străin, cu codul lui ca identificator al cumpărătorului (BT-46), fiindcă ANAF citește BT-47 doar ca CUI sau CNP românesc.
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:
- cel trimis de dumneavoastră, în
cursValutar(lei pentru o unitate) și, opțional,dataCurs; - altfel, cursul BNR valabil la data facturii: ultimul publicat de BNR înaintea ei. BNR publică la prânz cursul folosit din ziua bancară următoare, deci o factură de luni ia cursul de vineri. Monedele cotate la 100 de unități (HUF, JPY) sunt aduse la o unitate.
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:
- fiecare linie se convertește din suma ei în valută și se rotunjește la bani; prețul unitar în lei e linia împărțită la cantitate;
- totalul e suma liniilor rotunjite — poate diferi cu un ban de totalul în valută înmulțit direct cu cursul;
- XML-ul e o factură în lei obișnuită, cu mențiunea (pe PDF, la fel):
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țiune | Ce face |
|---|---|
POST /proformas | Creează, cu client inline ca la facturi |
GET /proformas | Listă, cu filtre externalId, anulata, facturata, from, to |
GET /proformas/:id | Returnează o proformă |
PUT /proformas/:id | Modifică; se schimbă doar câmpurile trimise |
POST /proformas/:id/anulare | Anulează, cu motiv opțional |
POST /proformas/:id/to-invoice | Converteș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țiune | Ce face |
|---|---|
POST /contracts | Creează, cu client inline ca la facturi |
GET /contracts | Listă, cu filtre stare, externalId, from, to |
GET /contracts/:id | Returnează un contract |
PUT /contracts/:id | Modifică; se schimbă doar câmpurile trimise |
POST /contracts/:id/anulare | Anulează, cu motiv opțional |
GET /contracts/:id/docx | Contractul ca document Word |
GET /contracts/:id/dpa | Acordul GDPR ca document Word |
POST /contracts/:id/emite-factura | Emite 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"
}
- intră facturile neanulate cu data emiterii în anul cerut; stornourile au totalurile cu minus, deci scad singure din sume;
- o factură în valută intră la cursul ei (
cursValutar), fiecare sumă rotunjită la bani; una în valută fără curs nu poate fi trecută în lei, deci nu intră în sume și se numără înfaraCurs; facturiși sumele cuprind doar facturile trecute în lei;peLuniare toate cele 12 luni, inclusiv pe cele fără facturi;anlipsă înseamnă anul curent, la ora României; un an în afara intervalului 2000–2100, sau nenumeric, primește400.
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.
| Cod | Când |
|---|---|
200 | Arhiva 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 |
502 | ANAF 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țiune | Ce face |
|---|---|
POST /spv/sincronizare | Aduce de la ANAF mesajele din ultimele zile zile (1–60, implicit 60) |
GET /spv/mesaje | Lista, cu filtre tip, from, to, cautare, limit, offset |
GET /spv/mesaje/:id | Un mesaj |
POST /spv/mesaje/:id/descarcare | Descarcă arhiva, citește factura sau eroarea, face PDF-ul |
GET /spv/mesaje/:id/pdf, /xml, /zip | Fișierele mesajului; îl descarcă întâi, dacă e nevoie |
POST /spv/descarcare | Descarcă î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âmp | Ce e |
|---|---|
tip | primita, trimisa, eroare, mesaj (de la cumpărător) sau altul; tipul exact dat de ANAF e în tipAnaf |
idDescarcare, indexIncarcare | id și id_solicitare din lista ANAF |
dataCreare | Momentul mesajului, ca dată UTC. ANAF îl scrie în ora României (202611121030); conversia ține cont de ora de vară |
cifEmitent, cifBeneficiar | Fără prefixul RO: din factura descărcată sau, până atunci, din detalii |
emitent, beneficiar, numarFactura, dataFactura, valoare | Citite din factură, după descărcare; null până atunci. valoare e totalul de plată, în moneda facturii |
mesajEroare | La 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
| Eveniment | Când |
|---|---|
invoice.submitted | Factura a fost încărcată la ANAF |
invoice.submit_failed | Încărcarea a eșuat |
invoice.validated | ANAF a validat factura |
invoice.rejected | ANAF 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 face | Unde |
|---|---|
| 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ărilor | butonul Șterge |
| Schimbarea URL-ului webhook | butonul Webhook |
| Regenerarea secretului webhook | din 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." }
- contează adresa conexiunii care ajunge la noi; antetele
X-Forwarded-ForsauX-Real-IPtrimise de integrare nu o pot schimba; - o adresă IPv4 scrisă ca IPv6 (
::ffff:18.199.208.221) e aceeași cu18.199.208.221; adresele se salvează în forma lor canonică, iar o intrare care nu e adresă sau interval valid e refuzată, cu numele ei în mesaj; - în lista de chei vedeți de la ce adresă a fost folosită ultima dată fiecare cheie și ultima adresă refuzată, fiecare cu butonul Adaugă această adresă. Cel mai simplu: folosiți cheia o dată din integrare, apoi restrângeți-o la adresa de la care a lucrat.
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ă.