Autentifică-te în aplicație
Cu contul firmei tale, pe efactura.docuhelp.ro.
Ghid de integrare pentru sisteme externe — WordPress/WooCommerce, OpenCart, ERP-uri proprii. Caută sau creează clienți (sau află datele unei firme după CUI, din ANAF), creează proforme și facturi cu TVA calculat automat, transformă o proformă în factură, descarcă PDF-ul sau XML-ul UBL trimis la ANAF. Iar când ANAF răspunde, afli printr-un webhook semnat — fără interogare periodică.
Format JSON · URL de bază https://efactura.docuhelp.ro/api/v2/ · Autentificare cu cheie API · Versiune document 2.2 · English version (PDF)
API v2 e separat de API v1
API v1 (/api/v1/invoice/add) e deja folosit de alte integrări și rămâne neschimbat — cele două nu se afectează reciproc și pot fi folosite în paralel. Pentru integrarea de bază, cu chei de acces și jurnal de trafic, vezi pagina API și integrări.
Nou în versiunea 2.2 a documentației
Chei doar pentru căutare firmă — fie secrete, pe serverul site-ului, fie publice, în browser, restrânse la domeniile tale — plus ordinea exactă a căutării, câmpul source și un exemplu de integrare de pe server. Din versiunea 2.1: GET /companies/{cui} — datele unei firme după CUI, ca să precompletezi clientul. Plus tabelul câmpurilor acceptate la crearea proformelor/facturilor, limitele de cereri și programul real de reîncercare al webhook-urilor. Din versiunea 2.0: Webhook-uri pentru statusul SPV — nu mai e nevoie de interogare periodică. Plus POST /invoices/{id}/send-spv pentru retrimiterea unei facturi existente, GET /invoices cu filtre pentru reconciliere și GET /invoices/{id}/xml pentru XML-ul UBL trimis la ANAF. Câmpurile vechi rămân neschimbate — integrările existente nu trebuie modificate.
Fiecare cerere către /api/v2/ trebuie să conțină Authorization: Bearer <cheia_dvs_api>.
Cu contul firmei tale, pe efactura.docuhelp.ro.
Introdu o denumire pentru cheie (ex: „Magazin WordPress”) și apasă Generează cheie nouă.
Cheia completă e afișată o singură dată, imediat după generare. Serverul reține doar un hash SHA-256, nu valoarea în clar — dacă ai pierdut-o, generezi una nouă și o revoci pe cea veche.
Fiecare cheie are un buton Revocă. Revocarea e imediată și ireversibilă: orice integrare care mai folosește acea cheie primește 403 Forbidden începând cu următoarea cerere.
La generare alegi tipul cheii. Cheile de căutare primesc 403 pe orice alt endpoint și nu pot citi sau crea nimic în contul tău. Fiecare cerere, inclusiv cele respinse, apare în pagina API din aplicație, cu IP-ul, răspunsul și ora.
| Tip | Prefix | Ce poate apela | Unde stă |
|---|---|---|---|
| Acces complet | dh_ | Tot API-ul v2 (facturi, proforme, clienți, webhook-uri, căutare firmă) și API-ul batch. | Doar pe serverul tău — secretă. |
| Căutare firmă, de pe server | dhls_ | Doar GET /api/v2/companies/{cui}. | Doar pe serverul tău — secretă. Pentru site-urile care fac căutarea prin propriul server, dar nu trebuie să țină o cheie care emite facturi. |
| Căutare firmă, din browser | dhpk_ | Doar GET /api/v2/companies/{cui}. | În JavaScript-ul paginii — publică, restrânsă la domeniile tale. |
Cheia de server (dhls_) se trimite doar în header-ul Authorization. Nu are verificare de domeniu: site-ul tău decide ce vizitatori pot declanșa o căutare (sesiune, nonce, captcha). Limite: 3.000 de căutări în 24 de ore pe cheie, plus limita firmei, de 60 pe minut. Nu există limite pe IP, pentru că toate cererile vin de pe IP-ul serverului tău — limitează-ți singur vizitatorii.
Cheia din browser (dhpk_) e pentru site-urile fără backend propriu. Merge doar din pagini aflate pe domeniile permise, verificate după header-ul Origin (sau Referer): magazin.ro permite și www.magazin.ro, iar *.magazin.ro orice subdomeniu. Se poate trimite și ca ?key=dhpk_... în URL (un GET simplu, fără preflight CORS). Limite: 10 căutări pe minut și 200 în 24 de ore pe IP-ul vizitatorului, 3.000 în 24 de ore pe cheie. ?refresh=1 e ignorat.
Cheia din browser e vizibilă oricui deschide sursa paginii — așa e gândită. Verificarea domeniului împiedică alte site-uri să o folosească, dar un script din afara unui browser poate falsifica header-ul Origin. De aceea cheia poate căuta doar date publice despre firme și are limite pe IP. Dacă site-ul are backend, preferă cheia de server.
| Aspect | Detalii |
|---|---|
| Format | Request și response: JSON (Content-Type: application/json), cu excepția /pdf (răspuns application/pdf) și /xml (răspuns application/xml). |
| URL de bază | https://efactura.docuhelp.ro/api/v2/ |
| ID-uri | Toate ID-urile (client, proformă, factură, webhook) sunt șiruri scurte (ex: a1B2c3D4e5F6), nu numere — folosite exact cum sunt returnate. |
| Sume | Prețurile din linii sunt fără TVA (nete). TVA-ul și totalul se calculează automat pe server. Orice total trimis de client e ignorat. |
| Date | Format YYYY-MM-DD (ex: 2026-08-24). |
| Valute | RON, EUR, USD, HUF. |
| Cod HTTP | Semnificație |
|---|---|
| 400 | Corp cerere JSON invalid/lipsă. |
| 401 | Lipsește header-ul Authorization. |
| 403 | Cheie API invalidă, revocată sau expirată; pentru o cheie de căutare (dhls_ / dhpk_): alt endpoint decât GET /companies/{cui}; pentru o cheie din browser, și o cerere de pe un domeniu nepermis. |
| 404 | Resursa (proformă/factură/webhook) nu există sau nu aparține firmei cheii folosite; la GET /companies/{cui}: CUI-ul nu a fost găsit la ANAF. |
| 409 | Conflict — ex: număr de factură/proformă deja folosit (cerere concurentă); proformă deja convertită în factură; factură deja acceptată în SPV. |
| 413 | Corpul cererii depășește 1 MB. |
| 422 | Date invalide (câmp obligatoriu lipsă, valoare greșită, client.id necunoscut, CUI cu format greșit sau firmă radiată). |
| 429 | Prea multe cereri — încetinește (limitare pe firmă, pe endpoint; la cheile din browser și pe IP-ul vizitatorului; vezi limitele de mai jos). |
| 500 | Eroare internă server. |
| 503 | Doar la GET /companies/{cui}: serverul ANAF nu răspunde momentan și firma nu există în baza noastră — reîncearcă mai târziu. |
Corpul răspunsului de eroare are mereu forma: {"error": "mesaj explicativ"}.
Limitarea se aplică pe firmă și pe endpoint, în ferestre de 60 de secunde. La depășire primești 429; așteaptă sfârșitul ferestrei și reîncearcă. Endpointurile de citire care nu apar mai jos nu au limită.
| Endpoint | Cereri pe minut |
|---|---|
POST /clients | 60 |
GET /companies/{cui} | 60 |
POST /proformas | 30 |
POST /proformas/{id}/convert | 30 |
POST /invoices | 30 |
POST /invoices/{id}/send-spv | 30 |
POST /webhooks | 20 |
POST /webhooks/{id}/test | 10 |
Cheile de căutare (dhls_, dhpk_) au limite suplimentare pe cheie, iar cele din browser și pe IP-ul vizitatorului — vezi tipurile de chei.
Verifică dacă cheia API e validă.
Seriile de facturare configurate pentru firmă, pentru a alege una la creare (sau lași serverul să aleagă prima serie configurată).
Următorul număr disponibil pentru o serie (informativ — numărul real se alocă automat, atomic, la creare).
Caută clienți după nume sau CIF. Se poate folosi și ?cif=. Potrivirea este parțială (conține textul căutat), iar răspunsul are cel mult 25 de rezultate. Fără q sau cif răspunsul este 422.
Creează un client nou, sau — dacă există deja un client cu același CIF la firma ta — returnează clientul existent (fără duplicat). Singurul câmp obligatoriu pentru un client nou e name.
Dacă ai deja client.id dintr-un apel anterior, îl poți trimite direct: {"client":{"id":"kL9mN2pQ7rS1"}}.
Caută o firmă după CUI și returnează datele ei, ca să precompletezi clientul (de exemplu când cumpărătorul a introdus doar CUI-ul în formularul de comandă). Nu creează nimic în contul tău — pentru asta trimiți apoi datele la POST /api/v2/clients. Merge cu o cheie completă sau de căutare de server (de pe serverul tău) și cu o cheie de căutare din browser.
Ordinea căutării: (1) baza noastră de date — registrul firmelor din România (importat de la ONRC) plus răspunsurile ANAF salvate la căutările anterioare; dacă avem firma și statusul ei TVA a fost verificat în ultimele 90 de zile, răspundem imediat, fără ANAF ("source":"local"). (2) Altfel ANAF, cu până la 3 încercări, iar răspunsul se salvează pentru data viitoare ("source":"anaf"). (3) Dacă ANAF nu răspunde, întoarcem ce avem local, chiar dacă e vechi; 503 primești doar când nu avem nimic nici local.
{cui}: doar cifre, cu sau fără prefixul RO, cel mult 10 cifre. Cheile răspunsului sunt aceleași cu câmpurile de la POST /api/v2/clients (name, cif, reg_com, address, city, subdivision, country, phone), deci le poți trimite direct, adăugând email etc. Pentru București, orașul vine ca SECTOR1…SECTOR6.
vat_payer e true / false, sau null dacă ANAF nu a răspuns și nu avem un status TVA salvat — null nu înseamnă „neplătitor”. Când ANAF nu răspunde, dar firma există în baza noastră de firme, răspundem cu datele de acolo (denumire, adresă, județ, nr. de înregistrare), cu vat_payer: null și un warning — statusul TVA trebuie atunci verificat separat. checked_on e momentul ultimei verificări la ANAF: datele sunt ținute în cache și reverificate automat după 90 de zile sau când lipsește statusul TVA; ?refresh=1 forțează reinterogarea ANAF (ignorat pentru cheile din browser). source: local = răspuns din baza noastră, anaf = ANAF a fost interogat pentru această cerere.
Erori: 404 CUI negăsit; 422 CUI cu format greșit sau firmă radiată; 503 ANAF nu răspunde și firma nu există nici în baza noastră (se întâmplă des — completează datele manual sau reîncearcă); 429 peste 60 de căutări pe minut (plus limitele cheilor de căutare); 403 pentru o cheie din browser folosită de pe un domeniu nepermis.
De pe serverul tău (recomandat când site-ul are backend): browserul apelează endpoint-ul tău, serverul tău verifică dacă cererea e legitimă și ne apelează cu o cheie secretă (dh_ sau dhls_). Limitează singur căutările pe vizitator — toate cererile ajung la noi de pe IP-ul serverului tău.
Din browser, cu o cheie dhpk_:
Creează o proformă nouă. Câmpuri acceptate în corpul cererii (aceleași și la POST /api/v2/invoices):
| Câmp | Tip | Oblig. | Descriere |
|---|---|---|---|
client | obiect | da | {"id":"..."} pentru un client existent, sau datele unui client nou (secțiunea 05). |
items | listă | da | Liniile documentului, de la 1 la 500 (secțiunea 05). |
date | text | nu | YYYY-MM-DD. Implicit data de azi (a serverului). |
series | text | nu | Cel mult 10 caractere. Implicit prima serie configurată în Setări Firmă (la proforme, prima serie de proformă). Dacă nu există nicio serie configurată și nu trimiți series, răspunsul este 422. |
currency | text | nu | RON, EUR, USD sau HUF. Implicit RON. |
obs | text | nu | Observații, afișate pe document. |
payby | text | nu | Scadența / termenul de plată (text liber, de obicei o dată YYYY-MM-DD), afișat pe document. |
mobilpay_allowed | boolean | nu | Implicit true. Dacă firma are plata online activată, documentul include datele pentru plata cu cardul. |
send_spv | boolean | nu | Doar la facturi (și la convert) — vezi secțiunea 06. Ignorat la proforme. |
Datele proformei, respectiv PDF-ul ei (generat la creare și păstrat pe server; dacă lipsește, se regenerează la cerere).
Linkul pdf_url din răspunsuri e semnat și valabil 30 de zile — îl poți pune direct într-un email sau în contul clientului, fără header de autentificare. PDF-urile nu pot fi afișate în <iframe> de pe alt domeniu (X-Frame-Options: SAMEORIGIN) — deschide-le în tab nou. Doar rutele /pdf acceptă link semnat; restul endpointurilor (inclusiv /xml) cer header-ul Authorization.
Transformă o proformă existentă în factură (de exemplu după confirmarea plății comenzii). O proformă poate fi convertită o singură dată.
Corpul cererii e opțional ({} e valid); câmpuri acceptate: date, series, payby, send_spv. Răspunsul are aceeași structură ca POST /api/v2/invoices, plus converted_from_proforma_id.
Creează direct o factură (fără să treacă prin proformă).
Starea curentă a facturii, inclusiv statusul real al trimiterii în SPV, respectiv PDF-ul ei.
Listează facturile emise, pentru reconciliere din magazin — util dacă sistemul tău a pierdut ID-urile. Fiecare element din items are exact aceeași structură ca GET /api/v2/invoices/{id}.
| Parametru | Descriere |
|---|---|
from, to | Interval de date (YYYY-MM-DD), inclusiv. |
series | Doar o anumită serie. |
spv | pending, accepted, rejected sau not_sent — filtrează după statusul SPV curent. |
limit, offset | Paginare. limit implicit 50, maxim 100. |
XML-ul UBL 2.1 exact așa cum a fost (sau ar fi) trimis la ANAF — pentru contabilitățile care arhivează documentul original, nu PDF-ul randat. Dacă XML-ul nu a fost încă generat, se generează la cerere.
Trimite în SPV o factură deja creată: fie ca reîncercare după o urcare eșuată (token ANAF expirat, ANAF indisponibil), fie ca trimitere ulterioară a unei facturi create fără send_spv.
Răspunsul 202 vine imediat și înseamnă „pus la trimis”, nu „acceptat de ANAF” — rezultatul real ajunge prin webhook sau la următorul GET /api/v2/invoices/{id}. Endpointul refuză cu 409 dacă factura e deja acceptată de ANAF sau dacă există deja o încărcare în curs. Dacă a fost respinsă, reîncercarea se face explicit, cu ?force=1, după corectarea datelor.
Înregistrează o adresă la care îți trimitem un POST semnat la fiecare schimbare, respectiv listează webhook-urile existente cu starea ultimei livrări (last_status, last_error, fail_count) și lista completă de evenimente disponibile (câmpul available_events).
Șterge webhook-ul și livrările lui aflate în coadă (răspuns {"ok":true}), respectiv trimite o livrare de probă sincron și returnează codul HTTP primit de la tine ({"ok":true,"http_status":200,"error":null}) — așa depanezi o integrare fără să aștepți un eveniment real.
Detalii complete — evenimente, structura livrării, verificarea semnăturii, reîncercări — în secțiunea 07.
| Câmp | Tip | Oblig. | Descriere |
|---|---|---|---|
name | text | da | Denumire produs/serviciu. |
description | text | nu | Descriere suplimentară (linia a 2-a pe factură). |
unit | text | nu | Unitate de măsură, cod UN/CEFACT (implicit H87 = bucată, dacă lipsește). Un cod nerecunoscut nu dă eroare — se afișează ca atare pe document. |
qty | număr | da | Cantitate. Poate fi negativă (linie de corecție/storno), dar nu zero. |
price | număr | da | Preț unitar fără TVA. |
vat_rate | număr | nu | Cotă TVA în procente. Ignorată dacă firma nu e plătitoare de TVA. |
vat_category | text | nu | S, Z, E, AE, K, G sau O. Dacă lipsește, se deduce din vat_rate. |
cpv | text | nu | Cod CPV, dacă e relevant. |
Categorii TVA: S = cotă standard/redusă, Z = cotă zero, E = scutit, AE = taxare inversă, K = livrare intracomunitară, G = export, O = în afara sferei TVA. Maximum 500 de linii pe document.
| Câmp | Descriere |
|---|---|
id | ID-ul unui client existent. Dacă e trimis, celelalte câmpuri sunt ignorate. |
name | Singurul câmp obligatoriu pentru un client nou. |
cif | CIF/CUI firmă, fără prefixul RO. Lipsa lui înseamnă persoană fizică. |
cnp | CNP persoană fizică, 13 cifre — opțional. |
address, city, subdivision, country | Adresa, orașul, județul (format RO-XX) și codul ISO de țară (implicit RO). Recomandate dacă factura va fi trimisă în SPV. |
email, phone, contact_person, reg_com | Opționale. |
currency | Valuta implicită a clientului, implicit RON. |
Un cif care există deja la clienții firmei tale reutilizează clientul existent — nu creează duplicate. Fără cif (persoane fizice), fiecare cerere poate crea un client nou — nu există deduplicare după nume sau CNP.
"send_spv": true, restul e asincron — în două etape.Răspunsul 201 ajunge imediat, cu spv.status = "not_sent" („pus la trimis”). Apoi, în câteva secunde, factura ajunge la ANAF și primește un index de încărcare — statusul devine pending. ANAF o validează și răspunde accepted sau rejected abia după minute sau ore.
Nu aștepta rezultatul final în câteva secunde: nu depinde de noi, ci de ANAF. Recomandat: înregistrează un webhook și primești ambele schimbări automat, în loc să interoghezi periodic.
Necesită ca firma să aibă deja conectat contul ANAF (OAuth) din interfața web. Fără conectare, trimiterea eșuează — factura în sine nu e afectată, iar cu POST /invoices/{id}/send-spv se poate reîncerca după configurare.
not_sent, pending, accepted sau rejected. Cel mai simplu câmp de folosit — câmpurile sent și accepted de mai jos rămân pentru compatibilitate.true dacă există o încercare de trimitere înregistrată.true dacă ANAF a acceptat factura.message.În loc să interoghezi GET /api/v2/invoices/{id} până când ANAF răspunde, înregistrezi o adresă la care îți trimitem un POST semnat de fiecare dată când se schimbă ceva. O singură cerere, o singură dată, la instalare.
| Eveniment | Când se emite |
|---|---|
invoice.spv.sent | Factura a ajuns la ANAF și are index de încărcare. Încă „în prelucrare”. |
invoice.spv.accepted | OK-ul final: ANAF a validat factura. |
invoice.spv.rejected | ANAF a respins factura; erorile sunt în spv.message. |
invoice.spv.failed | Urcarea în sine a eșuat (nu există index de încărcare) — ex: token ANAF expirat. |
invoice.created | Factură creată prin API v2 (direct sau din proformă). |
proforma.created | Proformă creată prin API v2. |
secret e afișat o singură dată, ca și cheia API — cu el verifici semnătura fiecărei livrări, deci salvează-l în configurarea integrării. Dacă îl pierzi, reînregistrezi același URL: secretul se rotește și primești unul nou. Câmpul events e opțional; dacă lipsește (sau nu conține niciun eveniment valid), primești toate evenimentele — numele necunoscute sunt ignorate.
https://, cu certificat valid; cel mult 500 de caractere și fără credențiale în adresă (user:parola@).localhost, 127.0.0.1, 10.x, 192.168.x, 169.254.x) sunt respinse la înregistrare — serverul nostru face cererea, deci o adresă internă ar fi o breșă de securitate.2xx.Adresa ta e publică, deci oricine ar putea trimite un POST acolo. Verifică întotdeauna semnătura înainte de a modifica ceva.
Semnătura se calculează peste șirul "{timestamp}.{corpul brut}", cu secret-ul primit la înregistrare. Folosește corpul brut, exact cum a venit — nu îl re-serializa din JSON, pentru că ordinea cheilor s-ar putea schimba și semnătura nu ar mai corespunde. Compară cu hash_equals(), nu cu ==.
2xx dacă ai primit livrarea. Orice altceva (sau un timeout de 15 secunde) înseamnă eșec.event_id — folosește-l pentru deduplicare, ca să nu procesezi de două ori același eveniment.2xx și atunci când nu recunoști factura — poate fi emisă din aplicație, nu din magazin. Un răspuns de eroare ar declanșa reîncercări inutile timp de circa 8 ore."test": true în corp și o factură fictivă — confirmă-le, dar nu modifica nicio comandă pe baza lor.O singură dată, la instalare: POST /webhooks. După aceea statusul SPV al fiecărei facturi ajunge singur la tine.
1. Caută clientul după CIF: GET /clients?cif=...
2. Dacă nu există, creează-l: POST /clients (dacă a dat doar CUI-ul, ia denumirea și adresa cu GET /companies/{cui})
3. Creează factura direct: POST /invoices, cu "send_spv": true dacă vrei trimitere automată
4. Descarci PDF-ul din pdf_url și îl atașezi la emailul de confirmare.
5. Statusul SPV vine singur, prin webhook — invoice.spv.sent, apoi accepted sau rejected. Fără webhook, îl citești cu GET /invoices/{id}.
1. La plasarea comenzii: POST /proformas.
2. Trimiți clientului PDF-ul proformei (pdf_url) ca instrucțiuni de plată.
3. La confirmarea plății (manual sau prin webhook propriu): POST /proformas/{id}/convert — proforma devine factură, fără să retastezi nimic.
dh_) în cod client (JavaScript din browser) — apelurile se fac din server, nu din browserul cumpărătorului. Singura cheie gândită pentru browser e cheia de căutare dhpk_; dacă serverul tău are nevoie doar de căutarea firmei, folosește o cheie dhls_ în locul uneia complete.202 de la send-spv ca pe o confirmare ANAF — înseamnă doar că trimiterea a fost pusă la rând.Adu-ne un exemplu de comandă din sistemul tău — ne uităm împreună la ce trimiți și cât de aproape ești de un apel funcțional. Pentru un conector construit complet la cerere, vezi dezvoltarea personalizată.
Fără obligații · Răspundem în aceeași zi lucrătoare