API-ul public 4invoices trimite date de facturare din sistemul tău în 4invoices și duce factura până la capăt în SPV-ul ANAF. Un singur host de producție, un flux client_credentials, scrieri fiscale idempotente și un înveliș de eroare identic pe toate rutele.
https://api.4invoices.net/v1 · Specificația OpenAPI (fără cheie)
Ce este API-ul 4invoices
- API-ul public 4invoices este REST peste HTTPS, cu JSON la intrare și la ieșire.
- Un singur host de producție: https://api.4invoices.net/v1. Nu există host de test separat.
- Specificația OpenAPI 3.1 este publică și nu cere cheie — o poți importa direct în Postman sau într-un generator de client.
- Fiecare răspuns poartă un antet X-Request-Id; citează-l când ceri suport.
- Toate scrierile fiscale sunt idempotente: o reîncercare după timeout nu emite un al doilea document.
- Circuit complet ANAF e-Factura prin API: preflight, depunere, urmărire, răspuns semnat și inbox de intrare. Niciun competitor românesc nu expune asta.
- SmartBill nu are niciun endpoint ANAF — trimiterea în SPV e doar o setare de cont din interfață — și nu are nici endpoint de listare a facturilor.
- Oblio poate crea clienți și produse doar ca efect secundar al emiterii unui document; nu ca resurse proprii.
- Generare de XML UBL / CIUS-RO fără depunere: obții documentul legal ca fișier, fără să atingi SPV-ul.
- Idempotency pe scrierile fiscale: retry-ul după timeout nu produce o a doua factură și nu consumă un al doilea număr.
- Inbox de facturi primite de la furnizori, citit programatic — baza pentru reconciliere automată.
Prima factură în 6 pași
- Generează o credențială în panou, la Facturare → Setări → Acces API, și notează client_secret — se afișează o singură dată.
- Completează profilul companiei și activează cel puțin o serie de tip fiscale, altfel emiterea returnează issuer_profile_incomplete sau no_series_configured.
- Generează o credențială în panou, la Facturare → Setări → Acces API, și notează client_secret — se afișează o singură dată.
- Completează profilul companiei și activează cel puțin o serie de tip fiscale, altfel emiterea returnează issuer_profile_incomplete sau no_series_configured.
- Schimbă credențiala pe un token: POST /v1/auth/token cu grant_type client_credentials. Tokenul e valabil o oră — pune-l în cache, nu cere unul la fiecare request.
- Emite factura: POST /v1/invoices cu antetul Idempotency-Key și vat_rate ca procent (21, nu 0.21).
- Rulează preflight-ul CIUS-RO cu POST /v1/invoices/{id}/efactura/validate, apoi trimite la ANAF cu POST /v1/invoices/{id}/efactura/submit.
- Urmărește starea cu GET /v1/efactura/submissions/{id} până ajunge în accepted sau rejected.
bash
# 1 — token (1 oră, ține-l în memoria cache)
ACCESS_TOKEN=$(curl -sS -X POST https://api.4invoices.net/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{"grant_type":"client_credentials","client_id":"cli_XXX","client_secret":"sec_XXX"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
# 2 — emite factura (Idempotency-Key este OBLIGATORIU)
curl -sS -X POST https://api.4invoices.net/v1/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-10457-invoice' \
-d '{"buyer":{"name":"Exemplu Comert SRL","tax_id":"RO12345678","county":"Cluj","city":"Cluj-Napoca","country":"RO"},
"lines":[{"description":"Abonament SaaS","quantity":1,"unit_price":400,"vat_rate":21}]}'
# 3 — preflight CIUS-RO, then send to ANAF
curl -sS -X POST https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/validate \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl -sS -X POST https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/submit \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Idempotency-Key: efactura-inv_01J7ABC-1'javascript
const BASE = "https://api.4invoices.net/v1";
const token = await fetch(`${BASE}/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.FOURINVOICES_CLIENT_ID,
client_secret: process.env.FOURINVOICES_CLIENT_SECRET,
}),
}).then((r) => r.json()).then((d) => d.access_token);
const invoice = await fetch(`${BASE}/invoices`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}-invoice`,
},
body: JSON.stringify({
buyer: { name: "Exemplu Comert SRL", tax_id: "RO12345678", county: "Cluj", city: "Cluj-Napoca", country: "RO" },
lines: [{ description: "Abonament SaaS", quantity: 1, unit_price: 400, vat_rate: 21 }],
currency: "RON",
metadata: { order_id: String(orderId) },
}),
}).then((r) => r.json());
console.log(invoice.number, invoice.totals); // "FCT-000123" { net, vat, gross }python
import os, uuid, requests
BASE = "https://api.4invoices.net/v1"
token = requests.post(f"{BASE}/auth/token", json={
"grant_type": "client_credentials",
"client_id": os.environ["FOURINVOICES_CLIENT_ID"],
"client_secret": os.environ["FOURINVOICES_CLIENT_SECRET"],
}, timeout=30).json()["access_token"]
headers = {
"Authorization": f"Bearer {token}",
# Câte o cheie pentru fiecare operație LOGICĂ — refolosește-o la reîncercare, nu o regenera.
"Idempotency-Key": f"order-{order_id}-invoice",
}
invoice = requests.post(f"{BASE}/invoices", headers=headers, json={
"buyer": {"name": "Exemplu Comert SRL", "tax_id": "RO12345678",
"county": "Cluj", "city": "Cluj-Napoca", "country": "RO"},
"lines": [{"description": "Abonament SaaS", "quantity": 1,
"unit_price": 400, "vat_rate": 21}], # procent, nu 0.21
"currency": "RON",
}, timeout=60).json()
print(invoice["number"], invoice["totals"])Autentificare
- Fluxul este client_credentials: schimbi client_id + client_secret pe un access_token de tip Bearer.
- Tokenul expiră în 3600 de secunde. Reînnoiește-l înainte de expirare, nu la fiecare apel — POST /auth/token are propriul rate limit pe IP.
- client_secret se stochează doar ca hash și nu poate fi recuperat. Dacă îl pierzi, generezi o credențială nouă și o revoci pe cea veche.
- Server-to-server exclusiv. Niciun origin de browser nu este permis prin CORS: dintr-un JavaScript de pagină, secretul ar ajunge la utilizator.
- Scope-urile se acordă per credențială. Emite credențiale separate pentru sisteme separate și dă fiecăreia strict minimul de scope-uri.
Facturare → Setări → Acces API
bash
curl -sS -X POST https://api.4invoices.net/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "cli_XXXXXXXXXXXX",
"client_secret": "sec_XXXXXXXXXXXXXXXXXXXX"
}'
# → {"access_token":"eyJ...","token_type":"Bearer","expires_in":3600,
# "scope":"invoices:read invoices:write efactura:submit"}javascript
// Păstrează tokenul în memoria cache; nu cere unul nou la fiecare cerere.
let cached = { token: "", expiresAt: 0 };
async function getToken() {
if (cached.token && Date.now() < cached.expiresAt - 60_000) return cached.token;
const res = await fetch("https://api.4invoices.net/v1/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.FOURINVOICES_CLIENT_ID,
client_secret: process.env.FOURINVOICES_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`token ${res.status}`);
const data = await res.json();
cached = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
return cached.token;
}Cotele de TVA sunt procente
vat_rate: 21 ✓ · vat_rate: 0.19 ✗ · vat_rate: 1 ✗- vat_rate este un PROCENT: 21 înseamnă 21%.
- Valorile din intervalul (0, 1] sunt respinse — 0.19 este o fracție, iar 1 este ambiguu între 1% și 100%.
- 0 este valid și înseamnă scutit sau taxare inversă; folosește notes pentru mențiunea legală.
- unit_price este prețul NET pe unitate atunci când emitentul este plătitor de TVA.
- Ai nevoie de totaluri înainte să emiți? POST /v1/invoices/preview-totals face aceeași aritmetică fără să consume un număr fiscal.
Idempotency pe scrierile fiscale
- Antetul Idempotency-Key este OBLIGATORIU pe POST /invoices, POST /invoices/{id}/storno și POST /invoices/{id}/efactura/submit.
- Cheia se ține minte 24 de ore. Reluarea aceleiași chei cu același body returnează rezultatul original și setează Idempotency-Replayed: true.
- Aceeași cheie cu un body diferit returnează 409 idempotency_key_reuse — nu suprascrie niciodată un document fiscal deja emis.
- O cheie per operațiune LOGICĂ, nu per încercare HTTP: order-10457-invoice, nu un UUID nou la fiecare reîncercare.
- Motivul e legal, nu tehnic: un număr fiscal consumat și o factură depusă în SPV nu se pot retrage.
bash
curl -sS -X POST https://api.4invoices.net/v1/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-10457-invoice' \
-d '{
"buyer": {
"name": "Exemplu Comert SRL",
"tax_id": "RO12345678",
"address": "Str. Fabricii 12",
"city": "Cluj-Napoca",
"county": "Cluj",
"country": "RO",
"email": "[email protected]"
},
"lines": [
{ "description": "Abonament SaaS, iulie", "quantity": 1, "unit_price": 400, "vat_rate": 21, "unit": "buc" },
{ "description": "Implementare", "quantity": 6, "unit_price": 250, "vat_rate": 21, "unit": "ora" }
],
"currency": "RON",
"document_kind": "factura",
"due_date": "2026-08-15",
"metadata": { "order_id": "10457" }
}'Toate endpoint-urile
| Metodă | Endpoint | Scope | Idempotency-Key | Cereri / minut |
|---|---|---|---|---|
| POST | /auth/token Schimbă credențialele pe un token de acces | — | — | 120 |
| GET | /me Tenant, scope-uri și limitele curente | invoices:read | — | 120 |
| POST | /invoices Emite o factură sau o proformă | invoices:write | obligatoriu | 30 |
| GET | /invoices Listează facturile (paginare cu cursor) | invoices:read | — | 120 |
| POST | /invoices/preview-totals Calculează totalurile fără să creeze nimic | invoices:read | — | 120 |
| GET | /invoices/{invoice_id} Returnează o factură | invoices:read | — | 120 |
| GET | /invoices/{invoice_id}/pdf Descarcă PDF-ul facturii | invoices:read | — | 120 |
| GET | /invoices/{invoice_id}/xml Descarcă XML-ul UBL / CIUS-RO | invoices:read | — | 120 |
| POST | /invoices/{invoice_id}/storno Emite o notă de credit care stornează această factură | invoices:write | obligatoriu | 30 |
| POST | /invoices/{invoice_id}/payments Marchează factura ca plătită | invoices:write | — | 30 |
| DELETE | /invoices/{invoice_id}/payments Marchează factura ca neplătită | invoices:write | — | 30 |
| GET | /clients Listează clienții | clients:read | — | 120 |
| POST | /clients Creează sau actualizează un client | clients:write | — | 30 |
| GET | /lookup/company/{tax_id} Validează un CUI în registrul ANAF | clients:read | — | 120 |
| GET | /series Listează seriile de numerotare | invoices:read | — | 120 |
| POST | /invoices/{invoice_id}/efactura/validate Preflight CIUS-RO — validează fără trimitere | efactura:read | — | 120 |
| POST | /invoices/{invoice_id}/efactura/submit Trimite factura la ANAF (SPV) | efactura:submit | obligatoriu | 10 |
| GET | /efactura/submissions Listează transmiterile către ANAF | efactura:read | — | 120 |
| GET | /efactura/submissions/{submission_id} Returnează o transmitere | efactura:read | — | 120 |
| POST | /efactura/submissions/{submission_id}/refresh Întreabă ANAF starea curentă | efactura:read | — | 10 |
| GET | /efactura/submissions/{submission_id}/download Descarcă arhiva răspunsului semnat | efactura:read | — | 120 |
| GET | /efactura/inbox Facturile primite de la furnizori prin SPV | efactura:read | — | 120 |
| GET | /efactura/inbox/{inbound_id} Returnează o factură primită | efactura:read | — | 120 |
| GET | /efactura/inbox/{inbound_id}/download Descarcă arhiva facturii primite | efactura:read | — | 120 |
| POST | /efactura/inbox/sync Trage mesajele noi din SPV în inbox | efactura:read | — | 10 |
25 endpoints · https://api.4invoices.net/v1/openapi.json
Scope-uri
- invoices:read
- invoices:write
- clients:read
- clients:write
- efactura:read
- efactura:submit
Circuitul ANAF e-Factura
- Diferențiatorul real: tot circuitul ANAF e-Factura se face prin API, nu doar generarea documentului.
- POST /invoices/{id}/efactura/validate rulează preflight-ul CIUS-RO și returnează exact câmpurile pe care ANAF le-ar respinge, fără să trimită nimic.
- GET /invoices/{id}/xml generează XML-ul UBL / CIUS-RO fără depunere — util pentru arhivare proprie sau pentru un audit intern.
- POST /invoices/{id}/efactura/submit depune în SPV. Este ireversibil: o a doua copie a aceleiași facturi nu se poate retrage din SPV.
- Ciclul de viață este normalizat: not_sent → validating → queued → sent → processing → accepted | rejected | error. accepted înseamnă transmis legal; error este o problemă de transport la noi și se poate reîncerca.
- GET /efactura/submissions/{id}/download aduce arhiva cu răspunsul semnat de ANAF.
- Inbox-ul de intrare există și el: POST /efactura/inbox/sync trage mesajele noi din SPV, iar GET /efactura/inbox listează facturile primite de la furnizori.
bash
# CIUS-RO preflight. Sends NOTHING to ANAF.
curl -sS -X POST "https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/validate?include_xml=true" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# → {"invoice_id":"inv_01J7ABC","valid":false,
# "errors":["buyer.county is required for RO buyers"],
# "xml_base64":"PD94bWwg..."}bash
curl -sS -X POST https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/submit \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Idempotency-Key: efactura-inv_01J7ABC-1'
# → {"submission_id":"sub_01J7XYZ","status":"queued"}python
import time, requests
BASE = "https://api.4invoices.net/v1"
TERMINAL = {"accepted", "rejected"}
H = {"Authorization": f"Bearer {token}"}
def wait_for_anaf(submission_id, timeout=900):
deadline = time.time() + timeout
delay = 15
while time.time() < deadline:
r = requests.get(f"{BASE}/efactura/submissions/{submission_id}", headers=H, timeout=30)
r.raise_for_status()
status = r.json()["status"]
if status in TERMINAL:
return r.json()
# /refresh asks ANAF directly and shares the 10/min submit bucket.
requests.post(f"{BASE}/efactura/submissions/{submission_id}/refresh", headers=H, timeout=30)
time.sleep(delay)
delay = min(delay * 2, 120)
raise TimeoutError(submission_id)Erori
- Orice 4xx sau 5xx are același înveliș: { "error": { type, code, message, param?, doc_url, request_id, errors? } }.
- type este clasa grosieră pe care ramifici codul („pot reîncerca?”), iar code este motivul stabil pe care îl mapezi în sistemul tău.
- param este calea punctată a câmpului vinovat, de exemplu lines.0.vat_rate.
- errors[] apare când există mai multe probleme pe câmpuri diferite în același request.
- request_id se loghează la noi. Citează-l în orice tichet de suport — cu el găsim requestul exact.
- Un 404 apare și pentru un id care aparține altui tenant: nu confirmăm niciodată existența datelor altcuiva.
json
{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "Datele trimise nu sunt valide.",
"param": "lines.0.vat_rate",
"doc_url": "https://4invoices.net/ro/api-dezvoltatori#errors",
"request_id": "req_8a893f2f33d4472da8d6e144",
"errors": [
{ "param": "lines.0.vat_rate", "message": "VAT rate must be a percent (21), not a fraction", "type": "value_error" }
]
}
}| Status | Tip | Cod |
|---|---|---|
| 401 | authentication_error | missing_bearer_token |
| 401 | authentication_error | invalid_client |
| 401 | authentication_error | token_expired |
| 401 | authentication_error | invalid_token |
| 401 | authentication_error | unauthorized |
| 403 | permission_error | insufficient_scope |
| 403 | permission_error | forbidden |
| 404 | not_found_error | not_found |
| 409 | conflict_error | idempotency_key_reuse |
| 409 | conflict_error | idempotency_in_progress |
| 409 | conflict_error | no_series_configured |
| 422 | invalid_request_error | validation_failed |
| 422 | invalid_request_error | issuer_profile_incomplete |
| 422 | invalid_request_error | cius_ro_incomplete |
| 422 | invalid_request_error | cannot_storno_draft |
| 422 | invalid_request_error | nothing_to_storno |
| 422 | invalid_request_error | invalid_json_number |
| 429 | rate_limit_error | rate_limit_exceeded |
| 502 | upstream_error | upstream_error |
| 503 | service_unavailable_error | service_unavailable |
Limite de rată
| Bucket | Cereri / minut | Ce înseamnă |
|---|---|---|
| read | 120 | GET *, POST /invoices/preview-totals, POST /invoices/{id}/efactura/validate |
| write | 30 | POST /invoices, POST /invoices/{id}/storno, POST|DELETE /invoices/{id}/payments, POST /clients |
| submit | 10 | POST /invoices/{id}/efactura/submit, POST /efactura/submissions/{id}/refresh, POST /efactura/inbox/sync |
- Limitele sunt per credențială, pe minut: 120 pentru citiri, 30 pentru scrieri, 10 pentru operațiunile ANAF (submit, refresh, inbox/sync).
- Antetele X-RateLimit-Limit, X-RateLimit-Remaining și X-RateLimit-Reset apar pe FIECARE răspuns, inclusiv pe 429.
- Pe 429 primești și Retry-After în secunde. Respectă-l; nu face retry în buclă strânsă.
- Bucketul de 10/min există pentru că ANAF limitează la aproximativ un apel de listare de mesaje pe minut per CIF.
- Pune /efactura/inbox/sync într-un cron, nu într-un poll agresiv.
- Ai nevoie de plafoane mai mari? Cere-le prin pagina de contact, cu volumul estimat pe zi.
X-RateLimit-Limit · X-RateLimit-Remaining · X-RateLimit-Reset · Retry-After
http
HTTP/2 429
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1785312000
Retry-After: 17
Content-Type: application/json
{"error":{"type":"rate_limit_error","code":"rate_limit_exceeded",
"message":"...","doc_url":"https://4invoices.net/ro/api-dezvoltatori#errors",
"request_id":"req_..."}}javascript
// Respectă Retry-After; nu reîncerca niciodată un 4xx în afară de 429.
async function call(url, init, attempt = 0) {
const res = await fetch(url, init);
if (res.status === 429 || res.status >= 500) {
if (attempt >= 5) return res;
const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return call(url, init, attempt + 1);
}
return res;
}
// O reîncercare de scriere TREBUIE să refolosească ACELAȘI Idempotency-Key, altfel emiți
// un al doilea document fiscal.Ghiduri pe subiect
Fiecare subiect are pagina lui, cu endpoint-urile, exemplele și erorile specifice.
- Autentificare în API-ul 4invoicesSchimbi client_id și client_secret pe un token Bearer valabil o oră, exclusiv server-to-server.
- Emiterea facturilor prin APIPOST /invoices cu Idempotency-Key, TVA în procente, plus listare, storno și marcare ca plătită.
- Clienți și verificarea CUI la ANAFInserare sau actualizare (upsert) deduplicată pe tax_id și căutare în registrul ANAF, ca e-Factura să nu fie respinsă.
- e-Factura ANAF prin APIPreflight CIUS-RO, depunere în SPV, stare normalizată, răspuns semnat și inbox de facturi primite.
- Serii de numerotare în APIFără o serie activă de tip fiscale, emiterea returnează 409 no_series_configured.
- Coduri de eroare și envelope-ul de eroareUn singur format pe toate rutele: type, code, message, param, doc_url, request_id și errors.
- Limite de rată și retry120 citiri, 30 scrieri și 10 operațiuni ANAF pe minut per credențială, cu antete X-RateLimit-*.
Greșeli frecvente
- Trimiterea TVA-ului ca fracție (0.19) sau ca 1. Ambele sunt respinse; trimite 19 sau 21.
- Cerere de token la fiecare request. Tokenul e valabil o oră; pune-l în cache sau vei lovi limita pe IP a endpointului de token.
- Generarea unei chei de idempotență noi la fiecare retry — exact scenariul care produce facturi duplicate.
- Apel din JavaScript de browser. CORS blochează orice origin, iar secretul ar fi expus oricum.
- Depunere în SPV fără preflight CIUS-RO. Un submit greșit nu se poate retrage.
- Poll pe /efactura/submissions/{id}/refresh mai des de 10 ori pe minut — bucketul e comun cu submit.
- Emitere înainte de completarea profilului de firmă și a unei serii fiscale active.
Întrebări frecvente de la dezvoltatori
Cum obțin credențiale pentru API-ul 4invoices?
Din panou, la Facturare → Setări → Acces API. Primești un client_id și un client_secret; secretul se afișează o singură dată și este stocat doar ca hash.
Cât este valabil access_token-ul?
3600 de secunde, adică o oră. Pune-l în cache și reînnoiește-l înainte de expirare — endpointul de token are limită proprie pe IP.
Cum trimit cota de TVA?
Ca procent: 21 pentru 21%. Valorile din intervalul (0, 1] sunt respinse, deci 0.19 și 1 dau eroare de validare.
Pe ce apeluri este obligatoriu Idempotency-Key?
Pe POST /invoices, POST /invoices/{id}/storno și POST /invoices/{id}/efactura/submit. Cheile se rețin 24 de ore, iar reluarea returnează documentul original cu Idempotency-Replayed: true.
Pot trimite o factură în e-Factura ANAF direct din API?
Da. Rulezi preflight-ul CIUS-RO cu /efactura/validate, depui cu /efactura/submit, urmărești starea normalizată pe /efactura/submissions/{id} și descarci răspunsul semnat de ANAF.
Care sunt limitele de rată?
Per credențială, pe minut: 120 citiri, 30 scrieri și 10 operațiuni ANAF. Fiecare răspuns are antete X-RateLimit-*, iar un 429 vine cu Retry-After.
Pot apela API-ul din frontend?
Nu. Este exclusiv server-to-server: niciun origin de browser nu trece prin CORS, iar client_secret nu are ce căuta în cod care ajunge la utilizator.
De ce primesc issuer_profile_incomplete la prima factură?
Profilul companiei nu este complet sau nu există o serie activă de tip fiscale. Completează-le în panou; API-ul refuză să producă un document fiscal invalid.