01Documentație tehnică · API v2

Documentație API e-Factura — proforme și facturi din sistemul tău, printr-un apel.

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.

02Autentificare

O cheie API, trimisă în header la fiecare cerere.

Fiecare cerere către /api/v2/ trebuie să conțină Authorization: Bearer <cheia_dvs_api>.

Autentifică-te în aplicație

Cu contul firmei tale, pe efactura.docuhelp.ro.

Mergi la Setări → tab „API”

Introdu o denumire pentru cheie (ex: „Magazin WordPress”) și apasă Generează cheie nouă.

Copiază cheia — o singură dată

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.

Revocare, oricând

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.

Tipuri de chei

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.

TipPrefixCe poate apelaUnde stă
Acces completdh_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 serverdhls_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 browserdhpk_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.

03Convenții generale
AspectDetalii
FormatRequest ș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-uriToate ID-urile (client, proformă, factură, webhook) sunt șiruri scurte (ex: a1B2c3D4e5F6), nu numere — folosite exact cum sunt returnate.
SumePrețurile din linii sunt fără TVA (nete). TVA-ul și totalul se calculează automat pe server. Orice total trimis de client e ignorat.
DateFormat YYYY-MM-DD (ex: 2026-08-24).
ValuteRON, EUR, USD, HUF.

Coduri de eroare

Cod HTTPSemnificație
400Corp cerere JSON invalid/lipsă.
401Lipsește header-ul Authorization.
403Cheie 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.
404Resursa (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.
409Conflict — ex: număr de factură/proformă deja folosit (cerere concurentă); proformă deja convertită în factură; factură deja acceptată în SPV.
413Corpul cererii depășește 1 MB.
422Date invalide (câmp obligatoriu lipsă, valoare greșită, client.id necunoscut, CUI cu format greșit sau firmă radiată).
429Prea multe cereri — încetinește (limitare pe firmă, pe endpoint; la cheile din browser și pe IP-ul vizitatorului; vezi limitele de mai jos).
500Eroare internă server.
503Doar 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"}.

Limite de cereri

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

EndpointCereri pe minut
POST /clients60
GET /companies/{cui}60
POST /proformas30
POST /proformas/{id}/convert30
POST /invoices30
POST /invoices/{id}/send-spv30
POST /webhooks20
POST /webhooks/{id}/test10

Cheile de căutare (dhls_, dhpk_) au limite suplimentare pe cheie, iar cele din browser și pe IP-ul vizitatorului — vezi tipurile de chei.

04Referință endpointuri

Sistem, clienți, proforme, facturi, webhook-uri.

Sistem

GET/api/v2/ping

Verifică dacă cheia API e validă.

curl https://efactura.docuhelp.ro/api/v2/ping \ -H "Authorization: Bearer dh_xxxxx" {"ok":true,"business_id":"a1B2c3D4e5F6","business_name":"Firma Mea SRL","vat_payer":true}
GET/api/v2/series

Seriile de facturare configurate pentru firmă, pentru a alege una la creare (sau lași serverul să aleagă prima serie configurată).

{"invoice":["EFA"],"proforma":["PRO"],"currencies":["RON","EUR","USD","HUF"]}
GET/api/v2/nextnr?type=invoice&series=EFA

Următorul număr disponibil pentru o serie (informativ — numărul real se alocă automat, atomic, la creare).

{"type":"invoice","series":"EFA","nr":"00123"}

Clienți

GET/api/v2/clients?q=...

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.

curl "https://efactura.docuhelp.ro/api/v2/clients?q=42979940" \ -H "Authorization: Bearer dh_xxxxx" {"items":[{"id":"kL9mN2pQ7rS1","name":"Client Exemplu SRL","cif":"42979940","email":"contact@exemplu.ro","city":"Bucuresti","country":"RO","currency":"RON"}]}
POST/api/v2/clients

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.

curl -X POST https://efactura.docuhelp.ro/api/v2/clients \ -H "Authorization: Bearer dh_xxxxx" -H "Content-Type: application/json" \ -d '{ "name": "Client Exemplu SRL", "cif": "42979940", "address": "Str. Exemplu nr. 1", "city": "Bucuresti", "subdivision": "RO-B", "country": "RO", "email": "contact@exemplu.ro", "currency": "RON" }' 201 Created {"id":"kL9mN2pQ7rS1","name":"Client Exemplu SRL","cif":"42979940","email":"contact@exemplu.ro","currency":"RON","country":"RO"}

Dacă ai deja client.id dintr-un apel anterior, îl poți trimite direct: {"client":{"id":"kL9mN2pQ7rS1"}}.

GET/api/v2/companies/{cui}

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.

curl https://efactura.docuhelp.ro/api/v2/companies/42979940 \ -H "Authorization: Bearer dh_xxxxx" {"name":"CLIENT EXEMPLU SRL","cif":"42979940","reg_com":"J40/1234/2020", "address":"Str. Exemplu, NR.1","city":"SECTOR2","subdivision":"RO-B", "country":"RO","phone":"","vat_payer":true, "checked_on":"2026-09-29","source":"local","warning":null}

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

<?php // cui-lookup.php - apelat din formularul de comandă: GET /cui-lookup.php?cui=RO42979940 session_start(); header('Content-Type: application/json'); // 1. doar vizitatorii tăi: un token emis odată cu pagina de comandă if (!hash_equals($_SESSION['checkout_token'] ?? '', $_GET['token'] ?? '')) { http_response_code(403); exit('{"error":"forbidden"}'); } // 2. limită proprie pe vizitator (aici: 10 căutări pe sesiune) $_SESSION['cui_lookups'] = ($_SESSION['cui_lookups'] ?? 0) + 1; if ($_SESSION['cui_lookups'] > 10) { http_response_code(429); exit('{"error":"too many lookups"}'); } // 3. validează CUI-ul înainte de a consuma un apel $cui = preg_replace('/\D/', '', $_GET['cui'] ?? ''); if ($cui === '' || strlen($cui) > 10) { http_response_code(422); exit('{"error":"invalid CUI"}'); } // 4. apelează Docuhelp cu cheia secretă (nu ajunge niciodată în browser) $ch = curl_init('https://efactura.docuhelp.ro/api/v2/companies/' . $cui); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, // ANAF poate răspunde greu CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('DOCUHELP_LOOKUP_KEY')], ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false) { http_response_code(502); exit('{"error":"lookup unavailable"}'); } http_response_code($code); // 200 / 404 / 422 / 429 / 503 - același înțeles ca mai sus echo $body; // name, address, city, subdivision, vat_payer, ...

Din browser, cu o cheie dhpk_:

// formularul de comandă: completează firma când cumpărătorul părăsește câmpul CUI const r = await fetch('https://efactura.docuhelp.ro/api/v2/companies/' + encodeURIComponent(cuiInput.value) + '?key=dhpk_xxxxx'); const c = await r.json(); if (r.ok) { form.company.value = c.name; form.address.value = c.address; form.city.value = c.city; } else { // c.error explică de ce; cumpărătorul completează datele manual }

Proforme

POST/api/v2/proformas

Creează o proformă nouă. Câmpuri acceptate în corpul cererii (aceleași și la POST /api/v2/invoices):

CâmpTipOblig.Descriere
clientobiectda{"id":"..."} pentru un client existent, sau datele unui client nou (secțiunea 05).
itemslistădaLiniile documentului, de la 1 la 500 (secțiunea 05).
datetextnuYYYY-MM-DD. Implicit data de azi (a serverului).
seriestextnuCel 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.
currencytextnuRON, EUR, USD sau HUF. Implicit RON.
obstextnuObservații, afișate pe document.
paybytextnuScadența / termenul de plată (text liber, de obicei o dată YYYY-MM-DD), afișat pe document.
mobilpay_allowedbooleannuImplicit true. Dacă firma are plata online activată, documentul include datele pentru plata cu cardul.
send_spvbooleannuDoar la facturi (și la convert) — vezi secțiunea 06. Ignorat la proforme.
curl -X POST https://efactura.docuhelp.ro/api/v2/proformas \ -H "Authorization: Bearer dh_xxxxx" -H "Content-Type: application/json" \ -d '{ "client": {"id": "kL9mN2pQ7rS1"}, "date": "2026-08-24", "currency": "RON", "obs": "Comanda #10452 din magazinul online", "items": [ {"name": "Produs A", "unit": "H87", "qty": 2, "price": 100, "vat_rate": 21}, {"name": "Transport", "unit": "H87", "qty": 1, "price": 20, "vat_rate": 21} ] }' 201 Created { "id": "pR3sT5uV7wX9", "series": "PRO", "nr": "00045", "date": "2026-08-24", "total": 266.20, "currency": "RON", "converted_to_invoice": false, "client": {"id": "kL9mN2pQ7rS1", "name": "Client Exemplu SRL"}, "pdf_url": "https://efactura.docuhelp.ro/api/v2/proformas/pR3sT5uV7wX9/pdf?exp=...&sig=..." }
GET/api/v2/proformas/{id} GET/api/v2/proformas/{id}/pdf

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.

POST/api/v2/proformas/{id}/convert

Transformă o proformă existentă în factură (de exemplu după confirmarea plății comenzii). O proformă poate fi convertită o singură dată.

curl -X POST https://efactura.docuhelp.ro/api/v2/proformas/pR3sT5uV7wX9/convert \ -H "Authorization: Bearer dh_xxxxx" -H "Content-Type: application/json" \ -d '{"send_spv": false}'

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.

Facturi

POST/api/v2/invoices

Creează direct o factură (fără să treacă prin proformă).

curl -X POST https://efactura.docuhelp.ro/api/v2/invoices \ -H "Authorization: Bearer dh_xxxxx" -H "Content-Type: application/json" \ -d '{ "client": {"id": "kL9mN2pQ7rS1"}, "date": "2026-08-24", "currency": "RON", "send_spv": true, "items": [ {"name": "Produs A", "unit": "H87", "qty": 2, "price": 100, "vat_rate": 21} ] }' 201 Created { "id": "fG2hJ4kL6mN8", "series": "EFA", "nr": "00123", "date": "2026-08-24", "total": 242.00, "currency": "RON", "paid": false, "client": {"id": "kL9mN2pQ7rS1", "name": "Client Exemplu SRL"}, "spv": {"status": "not_sent", "sent": false, "accepted": false, "index_incarcare": null, "document_id": null, "message": null, "error": null, "sent_at": null, "note": "SPV upload queued - subscribe to the invoice.spv.* webhooks, or poll GET /api/v2/invoices/{id}"}, "pdf_url": "https://efactura.docuhelp.ro/api/v2/invoices/fG2hJ4kL6mN8/pdf?exp=...&sig=..." }
GET/api/v2/invoices/{id} GET/api/v2/invoices/{id}/pdf

Starea curentă a facturii, inclusiv statusul real al trimiterii în SPV, respectiv PDF-ul ei.

GET/api/v2/invoices

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

ParametruDescriere
from, toInterval de date (YYYY-MM-DD), inclusiv.
seriesDoar o anumită serie.
spvpending, accepted, rejected sau not_sent — filtrează după statusul SPV curent.
limit, offsetPaginare. limit implicit 50, maxim 100.
curl "https://efactura.docuhelp.ro/api/v2/invoices?from=2026-08-01&spv=rejected" \ -H "Authorization: Bearer dh_xxxxx" {"items":[ ... ],"total":3,"limit":50,"offset":0}
GET/api/v2/invoices/{id}/xml

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.

POST/api/v2/invoices/{id}/send-spv

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.

curl -X POST https://efactura.docuhelp.ro/api/v2/invoices/fG2hJ4kL6mN8/send-spv \ -H "Authorization: Bearer dh_xxxxx" 202 Accepted

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.

Webhook-uri

POST/api/v2/webhooks GET/api/v2/webhooks

Î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).

DELETE/api/v2/webhooks/{id} POST/api/v2/webhooks/{id}/test

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

05Modele de date

Linii factură/proformă

CâmpTipOblig.Descriere
nametextdaDenumire produs/serviciu.
descriptiontextnuDescriere suplimentară (linia a 2-a pe factură).
unittextnuUnitate 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.
qtynumărdaCantitate. Poate fi negativă (linie de corecție/storno), dar nu zero.
pricenumărdaPreț unitar fără TVA.
vat_ratenumărnuCotă TVA în procente. Ignorată dacă firma nu e plătitoare de TVA.
vat_categorytextnuS, Z, E, AE, K, G sau O. Dacă lipsește, se deduce din vat_rate.
cpvtextnuCod 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.

Client

CâmpDescriere
idID-ul unui client existent. Dacă e trimis, celelalte câmpuri sunt ignorate.
nameSingurul câmp obligatoriu pentru un client nou.
cifCIF/CUI firmă, fără prefixul RO. Lipsa lui înseamnă persoană fizică.
cnpCNP persoană fizică, 13 cifre — opțional.
address, city, subdivision, countryAdresa, 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_comOpționale.
currencyValuta 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.

06Trimiterea în SPV

Adaugi "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.

status
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.
sent
true dacă există o încercare de trimitere înregistrată.
accepted
true dacă ANAF a acceptat factura.
index_incarcare
Numărul de încărcare SPV — numărul pe care îl comunici ANAF-ului când întrebi de o factură.
document_id
ID-ul documentului ANAF, după ce există un răspuns.
message
Mesajul de eroare, dacă ANAF a respins factura.
error
Vechiul câmp de eroare, păstrat pentru compatibilitate, cu aceeași valoare ca message.
sent_at
Momentul trimiterii.
07Webhook-uri

Nu mai întrebi periodic — te anunțăm noi.

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

Evenimente disponibile

EvenimentCând se emite
invoice.spv.sentFactura a ajuns la ANAF și are index de încărcare. Încă „în prelucrare”.
invoice.spv.acceptedOK-ul final: ANAF a validat factura.
invoice.spv.rejectedANAF a respins factura; erorile sunt în spv.message.
invoice.spv.failedUrcarea în sine a eșuat (nu există index de încărcare) — ex: token ANAF expirat.
invoice.createdFactură creată prin API v2 (direct sau din proformă).
proforma.createdProformă creată prin API v2.

Înregistrarea unui webhook

POST/api/v2/webhooks
curl -X POST https://efactura.docuhelp.ro/api/v2/webhooks \ -H "Authorization: Bearer dh_xxxxx" -H "Content-Type: application/json" \ -d '{ "url": "https://magazinul-meu.ro/wp-json/efactura-docuhelp/v1/webhook", "label": "Magazin WooCommerce", "events": ["invoice.spv.sent", "invoice.spv.accepted", "invoice.spv.rejected", "invoice.spv.failed"] }' 201 Created {"id":"wH1jK3lM5nP7","url":"https://magazinul-meu.ro/...","label":"Magazin WooCommerce", "events":[...],"active":true,"secret":"a3f1...<64 caractere hex>"}

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.

Cerințe pentru adresă

  • ✓Obligatoriu https://, cu certificat valid; cel mult 500 de caractere și fără credențiale în adresă (user:parola@).
  • ✓Trebuie să fie o adresă publică. Adresele locale sau private (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.
  • ✓Nu se urmăresc redirectări: răspunde direct cu 2xx.
  • ✓Un site local sau de test nu poate primi webhook-uri — folosește interogarea periodică pentru mediile de dezvoltare.

Structura unei livrări

POST /adresa-ta HTTP/1.1 Content-Type: application/json X-Docuhelp-Event: invoice.spv.accepted X-Docuhelp-Delivery: 4821 X-Docuhelp-Timestamp: 1787560982 X-Docuhelp-Signature: sha256=14582d25c33212dbe05c1376739eff9ff6c956ac3cba4865cdd08967e2fe3815 { "event": "invoice.spv.accepted", "event_id": "9f2c1a7b3e4d5f6a7b8c9d0e", "created_at": "2026-08-24T09:15:00+00:00", "data": { "business_id": "a1B2c3D4e5F6", "invoice": { "id": "fG2hJ4kL6mN8", "series": "EFA", "nr": "00123", "date": "2026-08-24", "total": 242.00, "currency": "RON", "paid": false, "client": {"id": "kL9mN2pQ7rS1", "name": "Client Exemplu SRL", "cif": "42979940"}, "pdf_url": "https://efactura.docuhelp.ro/api/v2/invoices/fG2hJ4kL6mN8/pdf?exp=...&sig=..." }, "spv": { "status": "accepted", "sent": true, "accepted": true, "index_incarcare": "5005559549", "document_id": "3007979966", "message": null, "sent_at": "2026-08-24 09:10:00" } } }

Verificarea semnăturii — obligatoriu

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

$raw = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_DOCUHELP_TIMESTAMP'] ?? 0; $sig = $_SERVER['HTTP_X_DOCUHELP_SIGNATURE'] ?? ''; // respinge livrările vechi (fereastră anti-replay de 5 minute) if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit; } $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret); if (!hash_equals($expected, $sig)) { http_response_code(401); exit; } // abia acum ai voie să folosești conținutul $event = json_decode($raw, true);

Reîncercări și deduplicare

  • ✓Răspunde cu 2xx dacă ai primit livrarea. Orice altceva (sau un timeout de 15 secunde) înseamnă eșec.
  • ✓Fiecare livrare are cel mult 6 încercări: cea inițială și 5 reîncercări, la intervale crescătoare de 1 min, 5 min, 30 min, 2 ore și 6 ore după eșecul precedent (în total, circa 8 ore și jumătate). După aceea livrarea se abandonează. Orele sunt orientative: livrările se trimit dintr-o coadă procesată periodic.
  • ✓După 25 de eșecuri consecutive, webhook-ul se dezactivează automat — un magazin șters nu e solicitat la nesfârșit. Se reactivează reînregistrând același URL.
  • ✓Reîncercările păstrează același event_id — folosește-l pentru deduplicare, ca să nu procesezi de două ori același eveniment.
  • ✓Răspunde 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.
  • ✓Livrările de probă au "test": true în corp și o factură fictivă — confirmă-le, dar nu modifica nicio comandă pe baza lor.
08Exemplu de flux complet

De la comandă WooCommerce/OpenCart la factură.

O singură dată, la instalare: POST /webhooks. După aceea statusul SPV al fiecărei facturi ajunge singur la tine.

Plată online, confirmată imediat

Pași

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ă

Rezultat

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

Plată offline (OP), confirmată ulterior

Pași

1. La plasarea comenzii: POST /proformas.
2. Trimiți clientului PDF-ul proformei (pdf_url) ca instrucțiuni de plată.

Rezultat

3. La confirmarea plății (manual sau prin webhook propriu): POST /proformas/{id}/convert — proforma devine factură, fără să retastezi nimic.

09Bune practici și securitate
  • ✓Nu expune o cheie completă (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.
  • ✓O cheie separată per integrare (magazin, mediu de test etc.) — ușor de revocat individual, fără să afectezi celelalte.
  • ✓Verifică întotdeauna semnătura webhook-urilor înainte de a modifica o comandă — vezi secțiunea 07.
  • ✓Tratează orice răspuns 4xx/5xx — nu presupune că factura a fost creată dacă nu primești 201 cu un id valid.
  • ✓La 409 Conflict la creare (foarte rar — cereri simultane pe aceeași serie), reîncearcă cererea, căci numerele se alocă atomic pe server. 409 la convert sau send-spv înseamnă stare deja finalizată și nu se rezolvă prin reîncercare.
  • ✓Păstrează ID-urile primite (client/proformă/factură) în comanda ta locală, pentru referințe ulterioare.
  • ✓Nu trata 202 de la send-spv ca pe o confirmare ANAF — înseamnă doar că trimiterea a fost pusă la rând.
10Întrebări frecvente

Despre API v2, pe scurt

Există un API pentru facturare electronică compatibil cu ANAF SPV?
Da. API v2 este o interfață REST în format JSON prin care creezi clienți, proforme și facturi direct din sistemul tău extern — WordPress/WooCommerce, OpenCart sau un ERP propriu — cu TVA calculat automat, PDF generat la cerere și trimitere opțională a facturii în ANAF SPV.
Cum mă autentific la API-ul de facturare?
Fiecare cerere către /api/v2/ trebuie să conțină header-ul Authorization: Bearer urmat de cheia API. Cheia se generează din aplicație, la Setări → tab API, este afișată o singură dată la generare, iar serverul reține doar un hash SHA-256 al ei, nu valoarea în clar.
Ce cod de eroare primesc dacă cheia API e invalidă sau revocată?
403 Forbidden — același cod pentru o cheie invalidă, revocată sau expirată. Dacă lipsește complet header-ul Authorization, răspunsul este 401.
Care e diferența dintre API v1 și v2?
API v1 (/api/v1/invoice/add) e deja folosit de alte integrări și rămâne neschimbat. API v2 este separat, cu endpointuri proprii pentru clienți, proforme și facturi. Cele două nu se afectează reciproc și pot fi folosite în paralel.
Trimiterea facturii în SPV e sincronă sau asincronă?
Este asincronă, în două etape. Răspunsul 201 la crearea facturii ajunge imediat, cu spv.status = not_sent. În câteva secunde factura ajunge la ANAF și primește un index de încărcare (status pending), iar validarea ANAF — acceptată sau respinsă — vine după minute sau ore, nu secunde. Recomandat: înregistrează un webhook și primești ambele schimbări automat, în loc să interoghezi periodic GET /api/v2/invoices/{id}.
Există webhook-uri pentru statusul e-Factura?
Da. Înregistrezi o adresă https publică printr-un singur POST /api/v2/webhooks și primești un POST semnat la fiecare schimbare: invoice.spv.sent, invoice.spv.accepted, invoice.spv.rejected, invoice.spv.failed, plus invoice.created și proforma.created. Fiecare livrare e semnată HMAC-SHA256 și are o fereastră anti-replay de 5 minute; livrările eșuate se reîncearcă (6 încercări în total, eșalonat pe circa 8 ore și jumătate).
Trebuie să îmi modific integrarea existentă pe API v2?
Nu. Toate câmpurile vechi rămân neschimbate — inclusiv spv.sent, spv.accepted și spv.error. Câmpurile noi (spv.status, document_id, message, sent_at) se adaugă lângă ele, iar webhook-urile sunt opționale: dacă nu înregistrezi niciunul, nu se schimbă nimic.
Cum retrimit în SPV o factură la care urcarea a eșuat?
Cu POST /api/v2/invoices/{id}/send-spv. Același endpoint trimite în SPV și o factură creată fără send_spv. Răspunde 202 imediat, ceea ce înseamnă „pus la trimis”, nu „acceptat de ANAF”. Pentru o factură respinsă de ANAF, reîncercarea se face explicit, cu ?force=1, după corectarea datelor.
Pot primi webhook-uri pe un site local sau de test?
Nu. Adresa trebuie să fie publică și pe https — adresele locale sau private (localhost, 127.0.0.1, 10.x, 192.168.x) sunt respinse la înregistrare, pentru că serverul nostru face cererea. Pentru mediile de dezvoltare folosește interogarea periodică: GET /api/v2/invoices/{id}.
Cum aflu datele unei firme după CUI?
Cu GET /api/v2/companies/{cui}. Căutăm întâi în baza noastră de firme și întrebăm ANAF doar când nu avem firma sau statusul ei TVA e mai vechi de 90 de zile. Răspunsul conține denumirea, adresa, județul, nr. de înregistrare și statusul de plătitor de TVA, cu aceleași chei ca la POST /api/v2/clients — deci poți precompleta clientul și apoi îl creezi. Dacă serverul ANAF nu răspunde, îți dăm datele din baza noastră de firme (cu vat_payer: null, statusul TVA necunoscut); 503 primești doar dacă firma nu e nici acolo.
Pot căuta firme după CUI direct din browser, fără server?
Da, cu o cheie de căutare din browser (prefix dhpk_), generată la Setări → tab API. Ea merge doar pe GET /api/v2/companies/{cui}, doar din pagini aflate pe domeniile alese de tine, și are limite pe IP-ul vizitatorului. Dacă site-ul are backend, e mai sigur să faci căutarea de pe server, cu o cheie de căutare de server (dhls_) sau cu cheia completă, și să verifici tu vizitatorii.
Ce se întâmplă dacă trimit de două ori același CIF la crearea unui client?
Nu se creează un duplicat. Un CIF care există deja la clienții firmei tale reutilizează clientul existent, în loc să creeze unul nou.
De ce primesc 409 Conflict la crearea unei facturi prin API?
La creare apare foarte rar, doar la cereri simultane pentru aceeași serie — reîncearcă cererea, căci numerele se alocă atomic pe server. 409 mai apare și la convert pe o proformă deja convertită sau la send-spv pe o factură deja acceptată în SPV; acolo nu se rezolvă prin reîncercare.

Ai nevoie de ajutor la integrare?

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