Dezvoltatori

API-ul eZif pentru magazine și aplicații (ML-08-A)

Cheile API, semnătura HMAC-SHA256, domeniile autorizate, emiterea facturilor, încasarea, callback-ul, storno, articolele și stocul, cu exemple PHP.

Specificația API (JSON)

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);

Documentația aplicației eZif 0.42.6.