API développeurs 4invoices

REST sur HTTPS, JSON, spécification OpenAPI publique. Facturation, clients, séries et circuit complet de facturation électronique nationale (ANAF e-Factura en Roumanie) — depuis votre ERP, boutique ou CRM.

L'API publique 4invoices envoie les données de facturation de votre système vers 4invoices et, en Roumanie, mène la facture jusqu'au bout dans le SPV de l'ANAF. Un seul hôte de production, un flux client_credentials, des écritures fiscales idempotentes et une enveloppe d'erreur identique sur toutes les routes.

https://api.4invoices.net/v1 · Spécification OpenAPI (sans clé)

Ce qu'est l'API 4invoices

  • L’API publique de 4invoices est une API REST sur HTTPS, avec du JSON en entrée et en sortie.
  • Un seul hôte de production : https://api.4invoices.net/v1. Il n’existe pas d’hôte de test séparé.
  • La spécification OpenAPI 3.1 est publique et accessible sans clé — importez-la directement dans Postman ou dans un générateur de clients.
  • Chaque réponse contient un en-tête X-Request-Id ; indiquez-le lorsque vous contactez le support.
  • Chaque écriture fiscale est idempotente : une nouvelle tentative après un timeout n’émet jamais un second document.
Serveur à serveur uniquement. N'appelez jamais l'API depuis du JavaScript de navigateur : le client_secret serait exposé au visiteur, et CORS bloque de toute façon toute origine.
  • Pour la Roumanie, le circuit e-Factura d’ANAF complet via l’API : contrôle préalable, dépôt, suivi, réponse signée et boîte de réception entrante. Aucun concurrent roumain ne propose cela.
  • SmartBill n’a aucun point de terminaison pour le dépôt dans le système national de facturation électronique (ANAF, en Roumanie) — l’envoi vers le SPV est un simple réglage de compte dans l’interface — et aucun point de terminaison de liste des factures.
  • Oblio ne peut créer des clients et des produits qu’en effet secondaire de l’émission d’un document, et non comme ressources à part entière.
  • Génération du XML UBL / CIUS-RO (Roumanie) sans dépôt : vous obtenez le document légal sous forme de fichier sans toucher au SPV.
  • Idempotence sur les écritures fiscales : une nouvelle tentative après un timeout ne produit ni seconde facture ni second numéro consommé.
  • Une boîte de réception des factures fournisseurs lisible par programme — la base d’un rapprochement automatisé.

Votre première facture en 6 étapes

Avant le premier appel
  1. Créez des identifiants dans le panneau, sous Facturation → Paramètres → Accès API, et copiez le client_secret — il n'est affiché qu'une seule fois.
  2. Complétez le profil de l'entreprise et activez au moins une série de type fiscale, sinon l'émission renvoie issuer_profile_incomplete ou no_series_configured.
  1. Créez des identifiants dans le panneau, sous Facturation → Paramètres → Accès API, et copiez le client_secret — il n'est affiché qu'une seule fois.
  2. Complétez le profil de l'entreprise et activez au moins une série de type fiscale, sinon l'émission renvoie issuer_profile_incomplete ou no_series_configured.
  3. Échangez les identifiants contre un token : POST /v1/auth/token avec grant_type client_credentials. Le token dure une heure — mettez-le en cache, n'en demandez pas un par requête.
  4. Émettez la facture : POST /v1/invoices avec un en-tête Idempotency-Key et vat_rate en pourcentage (21, pas 0.21).
  5. Pour le marché roumain, lancez le contrôle CIUS-RO avec POST /v1/invoices/{id}/efactura/validate, puis envoyez la facture à l'ANAF avec POST /v1/invoices/{id}/efactura/submit.
  6. Suivez l'état avec GET /v1/efactura/submissions/{id} jusqu'à ce qu'il atteigne accepted ou rejected.
bash
# 1 — token (1 heure, à mettre en 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 — émettre la facture (Idempotency-Key est OBLIGATOIRE)
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}",
    # Une clé par opération LOGIQUE — réutilisez-la au nouvel essai, ne la régénérez jamais.
    "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}],   # pourcentage, pas 0.21
    "currency": "RON",
}, timeout=60).json()

print(invoice["number"], invoice["totals"])

Authentification

  • Le flux est client_credentials : vous échangez client_id + client_secret contre un access_token de type Bearer.
  • Le jeton expire au bout de 3600 secondes. Renouvelez-le avant son expiration, pas à chaque appel — POST /auth/token a sa propre limite de débit par IP.
  • Le client_secret n’est stocké que sous forme de hash et ne peut pas être récupéré. Si vous le perdez, créez un nouvel identifiant et révoquez l’ancien.
  • Serveur à serveur uniquement. Aucune origine navigateur n’est autorisée par CORS : depuis le JavaScript d’une page, le secret fuiterait vers le visiteur.
  • Les scopes sont attribués par identifiant. Créez un identifiant distinct pour chaque système et donnez à chacun uniquement les scopes strictement nécessaires.

Facturation → Paramètres → Accès 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
// Mettez le token en cache ; n'en générez pas un par requête.
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;
}

Les taux de TVA sont des pourcentages

vat_rate: 21 ✓  ·  vat_rate: 0.19 ✗  ·  vat_rate: 1 ✗
  • vat_rate est un POURCENTAGE : 21 signifie 21 %.
  • Les valeurs dans l’intervalle (0, 1] sont refusées — 0.19 est une fraction, et 1 est ambigu entre 1 % et 100 %.
  • 0 est valide et signifie exonération ou autoliquidation ; placez la mention légale dans notes.
  • unit_price est le prix unitaire HORS TAXES lorsque l’émetteur est assujetti à la TVA.
  • Besoin des totaux avant de valider ? POST /v1/invoices/preview-totals effectue le même calcul sans consommer de numéro fiscal.

Idempotence sur les écritures fiscales

  • L’en-tête Idempotency-Key est OBLIGATOIRE sur POST /invoices, POST /invoices/{id}/storno et POST /invoices/{id}/efactura/submit.
  • Les clés sont conservées pendant 24 heures. Renvoyer la même clé avec le même corps retourne le résultat initial et positionne Idempotency-Replayed: true.
  • La même clé avec un corps différent renvoie 409 idempotency_key_reuse — un document fiscal déjà émis n’est jamais écrasé.
  • Une clé par opération LOGIQUE, pas par tentative HTTP : order-10457-invoice, et non un nouvel UUID à chaque nouvelle tentative.
  • La raison est juridique, pas technique : un numéro fiscal consommé et une facture déposée dans le système national de facturation électronique ne peuvent pas être retirés.
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" }
  }'

Tous les endpoints

MéthodeEndpointScopeIdempotency-KeyRequêtes / minute
POST/auth/token
Échanger les identifiants contre un token d'accès
——120
GET/me
Tenant, scopes et limites actuelles
invoices:read—120
POST/invoices
Émettre une facture ou une proforma
invoices:writeobligatoire30
GET/invoices
Lister les factures (pagination par curseur)
invoices:read—120
POST/invoices/preview-totals
Calculer les totaux sans rien créer
invoices:read—120
GET/invoices/{invoice_id}
Récupérer une facture
invoices:read—120
GET/invoices/{invoice_id}/pdf
Télécharger le PDF de la facture
invoices:read—120
GET/invoices/{invoice_id}/xml
Télécharger le XML UBL / CIUS-RO (Roumanie)
invoices:read—120
POST/invoices/{invoice_id}/storno
Émettre un avoir annulant cette facture
invoices:writeobligatoire30
POST/invoices/{invoice_id}/payments
Marquer la facture comme payée
invoices:write—30
DELETE/invoices/{invoice_id}/payments
Marquer la facture comme non payée
invoices:write—30
GET/clients
Lister les clients
clients:read—120
POST/clients
Créer ou mettre à jour un client
clients:write—30
GET/lookup/company/{tax_id}
Valider un CUI roumain dans le registre ANAF
clients:read—120
GET/series
Lister les séries de numérotation
invoices:read—120
POST/invoices/{invoice_id}/efactura/validate
Contrôle préalable CIUS-RO (Roumanie) — valider sans envoyer
efactura:read—120
POST/invoices/{invoice_id}/efactura/submit
Envoyer la facture à l'ANAF (SPV, Roumanie)
efactura:submitobligatoire10
GET/efactura/submissions
Lister les envois à l'ANAF (Roumanie)
efactura:read—120
GET/efactura/submissions/{submission_id}
Récupérer un envoi
efactura:read—120
POST/efactura/submissions/{submission_id}/refresh
Demander à l'ANAF (Roumanie) l'état actuel
efactura:read—10
GET/efactura/submissions/{submission_id}/download
Télécharger l'archive signée de la réponse
efactura:read—120
GET/efactura/inbox
Factures fournisseurs reçues via le SPV (Roumanie)
efactura:read—120
GET/efactura/inbox/{inbound_id}
Récupérer une facture reçue
efactura:read—120
GET/efactura/inbox/{inbound_id}/download
Télécharger l'archive de la facture reçue
efactura:read—120
POST/efactura/inbox/sync
Récupérer les nouveaux messages SPV (ANAF, Roumanie) dans la boîte de réception
efactura:read—10

25 endpoints · https://api.4invoices.net/v1/openapi.json

Scopes

  • invoices:read
  • invoices:write
  • clients:read
  • clients:write
  • efactura:read
  • efactura:submit

Le circuit ANAF e-Factura (Roumanie)

not_sentvalidatingqueuedsentprocessingacceptedrejectederror
  • Le vrai facteur de différenciation : pour la Roumanie, l’intégralité du circuit e-Factura d’ANAF passe par l’API, pas seulement la génération du document.
  • POST /invoices/{id}/efactura/validate exécute le contrôle préalable CIUS-RO (Roumanie) et renvoie précisément les champs qu’ANAF refuserait, sans rien envoyer.
  • GET /invoices/{id}/xml génère le XML UBL / CIUS-RO (le profil roumain) sans dépôt — utile pour votre propre archive ou un audit interne.
  • POST /invoices/{id}/efactura/submit dépose la facture dans le SPV (ANAF, Roumanie). C’est irréversible : une seconde copie de la même facture ne peut pas être retirée du SPV roumain.
  • Le cycle de vie est normalisé : not_sent → validating → queued → sent → processing → accepted | rejected | error. accepted signifie transmis légalement ; error est un échec de transport de notre côté, que l’on peut relancer sans risque.
  • GET /efactura/submissions/{id}/download récupère l’archive contenant la réponse signée de l’ANAF (Roumanie).
  • Le côté réception existe aussi : POST /efactura/inbox/sync récupère les nouveaux messages du SPV (ANAF, Roumanie) et GET /efactura/inbox liste les factures que vos fournisseurs vous ont envoyées.
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)

Erreurs

  • Toutes les réponses 4xx et 5xx utilisent la même enveloppe : { "error": { type, code, message, param?, doc_url, request_id, errors? } }.
  • type est la catégorie générale sur laquelle vous branchez votre logique (« puis-je réessayer ? »), et code est le motif stable que vous faites correspondre dans votre propre système.
  • param est le chemin pointé du champ en cause, par exemple lines.0.vat_rate.
  • errors[] apparaît lorsqu’une même requête présente des problèmes sur plusieurs champs.
  • request_id est journalisé de notre côté. Indiquez-le dans toute demande d'assistance — il identifie la requête exacte.
  • Un 404 est également renvoyé pour un id appartenant à un autre tenant : nous ne confirmons jamais l’existence des données de quelqu’un d’autre.
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" }
    ]
  }
}
StatutTypeCode
401authentication_errormissing_bearer_token
401authentication_errorinvalid_client
401authentication_errortoken_expired
401authentication_errorinvalid_token
401authentication_errorunauthorized
403permission_errorinsufficient_scope
403permission_errorforbidden
404not_found_errornot_found
409conflict_erroridempotency_key_reuse
409conflict_erroridempotency_in_progress
409conflict_errorno_series_configured
422invalid_request_errorvalidation_failed
422invalid_request_errorissuer_profile_incomplete
422invalid_request_errorcius_ro_incomplete
422invalid_request_errorcannot_storno_draft
422invalid_request_errornothing_to_storno
422invalid_request_errorinvalid_json_number
429rate_limit_errorrate_limit_exceeded
502upstream_errorupstream_error
503service_unavailable_errorservice_unavailable

Limites de débit

BucketRequêtes / minuteSignification
read120GET *, POST /invoices/preview-totals, POST /invoices/{id}/efactura/validate
write30POST /invoices, POST /invoices/{id}/storno, POST|DELETE /invoices/{id}/payments, POST /clients
submit10POST /invoices/{id}/efactura/submit, POST /efactura/submissions/{id}/refresh, POST /efactura/inbox/sync
  • Les limites s’appliquent par identifiant et par minute : 120 pour les lectures, 30 pour les écritures, 10 pour les opérations de facturation électronique (submit, refresh, inbox/sync).
  • X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset figurent sur CHAQUE réponse, y compris la 429.
  • Une 429 contient aussi Retry-After en secondes. Respectez-le ; ne réessayez pas dans une boucle serrée.
  • Le quota de 10/min existe parce qu’en Roumanie ANAF autorise elle-même environ un appel de liste de messages par minute et par CIF.
  • Placez /efactura/inbox/sync dans un cron, pas dans un polling agressif.
  • Besoin de plafonds plus élevés ? Faites-en la demande via le formulaire de contact en indiquant votre volume journalier prévu.

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
// Respectez Retry-After ; ne retentez jamais un 4xx autre que 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;
}

// Une nouvelle tentative d'écriture DOIT réutiliser le MÊME Idempotency-Key, sinon vous émettez
// un second document fiscal.

Guides par sujet

Chaque sujet a sa page, avec les endpoints, exemples et erreurs correspondants.

Erreurs fréquentes

  • Envoyer la TVA sous forme de fraction (0.19) ou de 1. Les deux sont refusés ; envoyez 19 ou 21.
  • Créer un jeton par requête. Le jeton est valable une heure ; mettez-le en cache, sinon vous atteindrez la limite par IP de l’endpoint de jeton.
  • Générer une nouvelle clé d’idempotence à chaque nouvelle tentative — précisément le scénario qui produit des factures en double.
  • Appeler l’API depuis du JavaScript dans le navigateur. CORS bloque toutes les origines, et le secret serait de toute façon exposé.
  • Déposer dans le système national de facturation électronique sans contrôle préalable (CIUS-RO en Roumanie). Un dépôt erroné ne peut pas être retiré.
  • Interroger /efactura/submissions/{id}/refresh plus de 10 fois par minute — ce quota est partagé avec submit.
  • Émettre avant que le profil de la société soit complet et qu’une série fiscale active existe.

Questions et réponses pour développeurs

Comment obtenir des identifiants pour l'API 4invoices ?

Dans le panneau, sous Facturation → Paramètres → Accès API. Vous recevez un client_id et un client_secret ; le secret n'est affiché qu'une fois et n'est stocké que sous forme de hash.

Quelle est la durée de validité de l'access_token ?

3600 secondes, soit une heure. Mettez-le en cache et renouvelez-le avant expiration — l'endpoint de token a sa propre limite par IP.

Comment envoyer le taux de TVA ?

En pourcentage : 21 pour 21 %. Les valeurs dans (0, 1] sont refusées, donc 0.19 et 1 échouent.

Quels appels exigent un Idempotency-Key ?

POST /invoices, POST /invoices/{id}/storno et POST /invoices/{id}/efactura/submit. Les clés sont mémorisées 24 heures et un rejeu renvoie le document original avec Idempotency-Replayed: true.

Puis-je soumettre une facture à l'e-Factura ANAF (Roumanie) directement via l'API ?

Oui, pour le marché roumain : contrôle CIUS-RO avec /efactura/validate, dépôt avec /efactura/submit, suivi du statut normalisé sur /efactura/submissions/{id} et téléchargement de la réponse signée de l'ANAF.

Quelles sont les limites de débit ?

Par identifiant et par minute : 120 lectures, 30 écritures et 10 opérations de facturation électronique. Chaque réponse porte les en-têtes X-RateLimit-*, et un 429 ajoute Retry-After.

Puis-je appeler l'API depuis mon frontend ?

Non. Elle est strictement serveur à serveur : aucune origine de navigateur ne passe CORS, et un client_secret n'a rien à faire dans du code livré au visiteur.

Pourquoi ai-je issuer_profile_incomplete sur ma première facture ?

Le profil de société est incomplet ou aucune série active de type fiscale n'existe. Complétez les deux dans le panneau ; l'API refuse de produire un document fiscal invalide.