API-ul extern leagă eZif de magazinele online și de aplicațiile proprii. Pentru o platformă care facturează prin eZif (proforma plătită devine factură, utilizator tehnic, drepturi, webhooks): secțiunea Aplicații conectate. Se configurează din Integrări → API și chei (Administratorul firmei). Documentele se emit prin aceleași reguli ca în interfață: numerotare pe serie, TVA, e-Factura, descărcarea stocului.
| Mediu | Adresă de bază |
|---|---|
| Dezvoltare | https://dev.ezif.ro/api/ext/v1 |
| Producție | https://app.ezif.ro/api/ext/v1 |
Autentificarea
Fiecare aplicație are cheia ei (Integrări → API și chei → Creează cheia). La creare primești X-Ezif-Key (ezk_…) și secretul (ezs_…). Cheia se vede și se copiază oricând din listă; secretul se afișează din nou cu Arată secretul, iar Secret nou îl înlocuiește (cheia rămâne aceeași, secretul vechi nu mai merge imediat) — ambele cer codul 2FA al Administratorului și rămân în Jurnalul de audit. Documentele se emit în numele utilizatorului care a creat cheia; dacă acesta pierde dreptul de emitere, cheia se oprește.
Fiecare cerere are 3 antete:
| Antet | Valoare |
|---|---|
X-Ezif-Key |
cheia (ezk_…) |
X-Ezif-Timestamp |
ora cererii, secunde Unix (UTC); se acceptă cel mult 5 minute diferență |
X-Ezif-Signature |
HMAC-SHA256 în hex, cu secretul, peste textul: timestamp + \n + METODA + \n + calea + \n + sha256(corpul cererii) |
Calea este partea de după domeniu (ex. /api/ext/v1/invoices), fără parametrii de după ?. La GET corpul e gol (sha256 al șirului gol).
O cheie revocată nu mai funcționează imediat și se poate apoi șterge din listă (rămâne în Jurnalul de audit). Pagina Integrări → API și chei are casete pliabile (Chei API, Ultimele cereri, Domenii autorizate, Callback, Articole și clienți, Compatibil FGO), toate închise la deschiderea paginii, cu un rezumat în antet; fiecare casetă se salvează separat, iar domeniile se salvează imediat la adăugare și la ștergere.
Produs necunoscut (codul liniei nu există): setarea din Articole și clienți — linie liberă (implicit), articol nou fără gestiune de stoc sau articol nou cu scădere din stoc; articolele create astfel sunt marcate „de verificat”.
Domeniile autorizate: dacă lista nu e goală, aplicația își trimite adresa în antetul X-Ezif-Site (ex. https://bioglobal.ro); se acceptă domeniul și subdomeniile lui.
Jurnalul cererilor: Integrări → API și chei → Ultimele cereri arată fiecare cerere (ora, cheia, site-ul, metoda și calea, codul, motivul respingerii, durata); se păstrează 30 de zile.
Limita de cereri: cel mult 120 de cereri pe minut pentru o cheie; peste ea, răspunsul e 429 cu antetul Retry-After (secundele de așteptat).
Erorile au formatul obișnuit eZif: {"title": "…", "status": 4xx, "code": "…"} — 401 la cheie/semnătură/oră, 403 la domeniu sau la drepturi, 422 la date invalide, 404 la document inexistent.
Metodele
| Metoda | Cale | Ce face |
|---|---|---|
| GET | /ping |
verifică cheia și semnătura; întoarce firma |
| POST | /invoices |
emite o factură (sau proformă) |
| GET | /invoices/{id} |
starea: număr, total, încasat, stare document, stare SPV, link PDF |
| GET | /invoices/by-external/{id} |
factura unei comenzi din magazin ({id} = external_id) |
| GET | /invoices/{id}/pdf |
PDF-ul facturii |
| POST | /invoices/{id}/storno |
storno total ({"reason": "…"}) |
| POST | /invoices/{id}/payments |
înregistrează o încasare |
| GET | /series |
seriile active pe tipuri: {"invoice": [{"prefix": "BIO", "default": true}], "proforma": [...]} |
| POST | /invoices/{id}/convert |
proforma emisă → factură emisă (aceleași linii și client); {"external_id": "…"} opțional; a doua cerere întoarce aceeași factură |
| POST | /callback |
aplicația își înregistrează adresa de callback ({"url": "https://…"}) pe cheia ei; răspunsul conține secretul cu care eZif semnează apelurile (la re-salvare, același secret; "rotate": true = secret nou) |
| POST | /callback/test |
eZif apelează imediat callback-ul cheii și răspunde {"ok": true} sau motivul |
| GET | /items |
articolele, cu codurile de bare și stocul total; parametri: page, per_page (max. 500), changed_since (ex. 2026-09-01T00:00:00Z = doar cele modificate) |
| GET | /items/{sku} |
un articol |
| GET | /stock |
stocul pe articol × gestiune: fizic, rezervat, disponibil; parametri opționali warehouse (ex. GES01) și sku (lista codurilor, separate prin virgulă — ex. produsele unei comenzi) |
Emiterea facturii
{
"external_id": "WC-1001",
"series": "BIO",
"currency": "RON",
"price_mode": "gross",
"due_days": 0,
"client": {"name": "Ion Popescu", "email": "ion@example.ro", "phone": "0721000000",
"country": "RO", "county": "Cluj", "city": "Cluj-Napoca", "street": "Str. Unirii 1"},
"lines": [
{"code": "LAM-120", "name": "LAMININE 120 capsule", "qty": 2, "price": 450.00, "vat": 21},
{"name": "Transport", "qty": 1, "price": 15.99, "vat": 21}
]
}
| Câmp | Explicație |
|---|---|
external_id |
numărul comenzii din magazin. Aceeași comandă nu produce două facturi: a doua cerere întoarce factura existentă, cu "duplicate": true. |
series |
prefixul seriei din eZif (Nomenclatoare → Serii de documente); lipsă = seria implicită |
kind |
invoice (implicit) sau proforma |
price_mode |
net = prețuri fără TVA (implicit), gross = prețuri cu TVA |
issue_date, due_date / due_days |
lipsă = azi / termenul clientului |
client |
persoană juridică: cui (cu RO = plătitor de TVA), reg_com; persoană fizică: fără cui, opțional cnp. Adresa (județ, localitate, stradă) este obligatorie — apare pe e-Factura. |
lines[].code |
codul articolului din eZif. Găsit → linia folosește articolul (stoc, categorie); negăsit sau lipsă → linie liberă. |
lines[].vat |
procentul (21, 11, 0) sau codul cotei (ex. SCUTIT) |
lines[].qty |
cantitatea; −1 pe o linie de reducere (ex. {"name": "Reducere", "qty": -1, "price": 30, "vat": 21}) — permis doar fără articol de stoc (linie liberă sau serviciu), iar totalul facturii rămâne pozitiv; returul de marfă se face prin storno |
lines[].warehouse |
codul gestiunii din care se descarcă marfa (ex. GES01); lipsă = gestiunea implicită a articolului |
lines[].service |
true pentru transport / reducere: dacă articolul cu acest cod nu există și regula firmei îl adaugă automat, se creează ca serviciu, fără stoc |
Clientul se caută după setarea firmei: după CUI / CNP, apoi după e-mail (implicit), doar după CUI, sau client nou la fiecare factură. Negăsit → se creează în Parteneri.
Codul de articol existent cu altă denumire: se folosește articolul existent (implicit), se actualizează denumirea în eZif sau factura se respinge. Cu un separator de prefix (ex. -), codul LAM-120-X caută articolul LAM.
Totalul cu TVA: eZif calculează TVA pe totalul fiecărei cote (regula facturilor românești), iar unele magazine o calculează pe fiecare produs. De aceea, la prețuri cu TVA, totalul poate diferi cu 1 ban de cel al comenzii; răspunsul arată totalul exact al facturii.
Răspunsul (201):
{"id": 812, "kind": "invoice", "series": "BIO", "number": "154", "number_text": "BIO00154", "issue_date": "2026-09-29",
"total": "915.99", "paid": "0.00", "document_status": "issued", "payment_status": "unpaid", "spv_status": "queued",
"external_id": "WC-1001", "pdf_url": "https://dev.ezif.ro/api/public/documente/…"}
pdf_url este un link public, fără autentificare, bun de trimis clientului.
Încasarea
{"amount": 915.99, "paid_on": "2026-09-29", "method": "card", "reference": "Netopia 12345", "external_id": "PAY-1001"}
method: card, online, transfer, numerar, alta. Cu external_id, aceeași plată nu se înregistrează de două ori. Se încasează doar facturile emise.
Callback la încasare
Destinația: callback-ul cheii care a emis factura (înregistrat de aplicație cu POST /callback); dacă cheia nu are unul, URL callback general al firmei (Integrări → Callback la încasare, cu Arată secretul pe cod 2FA). eZif trimite POST (JSON) la fiecare încasare:
{"event": "invoice.paid", "invoice_id": 812, "number": "BIO00154", "external_id": "WC-1001", "amount": "915.99",
"paid_total": "915.99", "payment_status": "paid", "paid_on": "2026-09-29", "sent_at": "2026-09-29T10:15:02+00:00"}
event este invoice.paid (încasată integral), invoice.payment (parțial) sau invoice.payment_cancelled (o încasare anulată în eZif). Încasările vin din API sau din eZif (Documente de vânzare → factura → Încasări). Antete: X-Ezif-Timestamp și X-Ezif-Signature = HMAC-SHA256 hex cu secretul de callback, peste timestamp + . + corpul exact primit. Răspunsul aplicației trebuie să fie 2xx; altfel eZif reîncearcă după 1 min, 5 min, 15 min, 1 h, 3 h, 12 h, apoi marchează „eșuat” (vizibil în Integrări). Trimite un test verifică adresa.
Exemplu PHP
<?php
const EZIF = 'https://dev.ezif.ro';
const KEY = 'ezk_…'; // din Integrări → API și chei
const SECRET = 'ezs_…';
function ezif(string $method, string $path, ?array $body = null): array {
$raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$sig = hash_hmac('sha256', $ts . "\n" . $method . "\n" . $path . "\n" . hash('sha256', $raw), SECRET);
$ch = curl_init(EZIF . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30,
CURLOPT_POSTFIELDS => $raw ?: null, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Ezif-Key: ' . KEY,
'X-Ezif-Timestamp: ' . $ts, 'X-Ezif-Signature: ' . $sig, 'X-Ezif-Site: https://magazin.ro']]);
$res = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) throw new RuntimeException($res['title'] ?? "eZif HTTP $code");
return $res;
}
$f = ezif('POST', '/api/ext/v1/invoices', ['external_id' => 'WC-1001', 'price_mode' => 'gross',
'client' => ['name' => 'Ion Popescu', 'email' => 'ion@example.ro', 'county' => 'Cluj', 'city' => 'Cluj-Napoca', 'street' => 'Str. Unirii 1'],
'lines' => [['code' => 'LAM-120', 'name' => 'LAMININE 120 capsule', 'qty' => 2, 'price' => 450, 'vat' => 21]]]);
ezif('POST', "/api/ext/v1/invoices/{$f['id']}/payments", ['amount' => (float) $f['total'], 'method' => 'card', 'external_id' => 'PAY-1001']);
Verificarea callback-ului în PHP:
$raw = file_get_contents('php://input');
$ok = hash_equals(hash_hmac('sha256', $_SERVER['HTTP_X_EZIF_TIMESTAMP'] . '.' . $raw, CALLBACK_SECRET), $_SERVER['HTTP_X_EZIF_SIGNATURE'] ?? '');
if (!$ok || abs(time() - (int) $_SERVER['HTTP_X_EZIF_TIMESTAMP']) > 300) { http_response_code(401); exit; }
$e = json_decode($raw, true); // $e['external_id'] = comanda, $e['payment_status'] = 'paid'
http_response_code(200);