API para desarrolladores de 4invoices

REST sobre HTTPS, JSON, especificación OpenAPI pública. Facturación, clientes, series y el circuito completo de facturación electrónica nacional (ANAF e-Factura en Rumanía) — desde tu ERP, tienda o CRM.

La API pública de 4invoices envía los datos de facturación de tu sistema a 4invoices y, en Rumanía, lleva la factura hasta el final en el SPV de ANAF. Un único host de producción, un flujo client_credentials, escrituras fiscales idempotentes y una misma envoltura de error en todas las rutas.

https://api.4invoices.net/v1 · Especificación OpenAPI (sin clave)

Qué es la API de 4invoices

  • La API pública de 4invoices es REST sobre HTTPS, con JSON de entrada y JSON de salida.
  • Un único host de producción: https://api.4invoices.net/v1. No existe un host de pruebas aparte.
  • La especificación OpenAPI 3.1 es pública y no requiere clave: impórtala directamente en Postman o en un generador de clientes.
  • Cada respuesta incluye la cabecera X-Request-Id; indícala cuando solicites soporte.
  • Toda escritura fiscal es idempotente: un reintento tras un timeout nunca emite un segundo documento.
Solo servidor a servidor. Nunca llames a la API desde JavaScript de navegador: el client_secret llegaría al visitante y CORS bloquea cualquier origen de todos modos.
  • Para Rumanía, el ciclo completo de e-Factura con ANAF a través de la API: validación previa, envío, seguimiento, respuesta firmada y bandeja de entrada. Ningún competidor rumano lo ofrece.
  • SmartBill no tiene ningún endpoint para la presentación en el sistema nacional de facturación electrónica (ANAF, en Rumanía) —el envío al SPV es una opción de la cuenta disponible solo en la interfaz— ni endpoint alguno para listar facturas.
  • Oblio solo puede crear clientes y productos como efecto secundario de emitir un documento, no como recursos de primer nivel.
  • Generación de XML UBL / CIUS-RO (Rumanía) sin presentarlo: obtienes el documento legal como archivo sin tocar el SPV rumano.
  • Idempotencia en las escrituras fiscales: un reintento tras un timeout no genera una segunda factura ni consume un segundo número.
  • Una bandeja de entrada de facturas de proveedores legible por programa: la base para una conciliación automatizada.

Tu primera factura en 6 pasos

Antes de la primera llamada
  1. Genera una credencial en el panel, en Facturación → Ajustes → Acceso API, y copia el client_secret — solo se muestra una vez.
  2. Completa el perfil de la empresa y activa al menos una serie de tipo fiscale; si no, la emisión devuelve issuer_profile_incomplete o no_series_configured.
  1. Genera una credencial en el panel, en Facturación → Ajustes → Acceso API, y copia el client_secret — solo se muestra una vez.
  2. Completa el perfil de la empresa y activa al menos una serie de tipo fiscale; si no, la emisión devuelve issuer_profile_incomplete o no_series_configured.
  3. Cambia la credencial por un token: POST /v1/auth/token con grant_type client_credentials. El token dura una hora — guárdalo en caché, no pidas uno por petición.
  4. Emite la factura: POST /v1/invoices con la cabecera Idempotency-Key y vat_rate como porcentaje (21, no 0.21).
  5. Para el mercado rumano, ejecuta la verificación CIUS-RO con POST /v1/invoices/{id}/efactura/validate y envíala a ANAF con POST /v1/invoices/{id}/efactura/submit.
  6. Sigue el estado con GET /v1/efactura/submissions/{id} hasta que llegue a accepted o rejected.
bash
# 1 — token (1 hora, guárdalo en caché)
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 — emitir la factura (Idempotency-Key es OBLIGATORIO)
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}",
    # Una clave por operación LÓGICA: reutilízala al reintentar, nunca la regeneres.
    "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}],   # porcentaje, no 0.21
    "currency": "RON",
}, timeout=60).json()

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

Autenticación

  • El flujo es client_credentials: intercambias client_id + client_secret por un access_token de tipo Bearer.
  • El token caduca en 3600 segundos. Renuévalo antes de que caduque, no en cada llamada: POST /auth/token tiene su propio límite de peticiones por IP.
  • El client_secret se almacena solo como hash y no se puede recuperar. Si lo pierdes, generas una credencial nueva y revocas la anterior.
  • Solo servidor a servidor. CORS no admite ningún origen de navegador: desde el JavaScript de una página, el secreto quedaría expuesto al visitante.
  • Los scopes se conceden por credencial. Emite una credencial distinta para cada sistema y concede a cada una solo los scopes mínimos que necesita.

Facturación → Ajustes → Acceso 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
// Guarda el token en caché; no generes uno por petición.
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;
}

Los tipos de IVA son porcentajes

vat_rate: 21 ✓  ·  vat_rate: 0.19 ✗  ·  vat_rate: 1 ✗
  • vat_rate es un PORCENTAJE: 21 significa 21%.
  • Se rechazan los valores en (0, 1]: 0.19 es una fracción, y 1 es ambiguo entre 1% y 100%.
  • 0 es válido y significa exento o inversión del sujeto pasivo; incluye la mención legal en notes.
  • unit_price es el precio unitario NETO cuando el emisor es sujeto pasivo del IVA.
  • ¿Necesitas los totales antes de confirmar? POST /v1/invoices/preview-totals ejecuta el mismo cálculo sin consumir un número fiscal.

Idempotencia en las escrituras fiscales

  • La cabecera Idempotency-Key es OBLIGATORIA en POST /invoices, POST /invoices/{id}/storno y POST /invoices/{id}/efactura/submit.
  • Las claves se recuerdan durante 24 horas. Repetir la misma clave con el mismo cuerpo devuelve el resultado original y establece Idempotency-Replayed: true.
  • La misma clave con un cuerpo distinto devuelve 409 idempotency_key_reuse: un documento fiscal ya emitido nunca se sobrescribe.
  • Una clave por operación LÓGICA, no por intento HTTP: order-10457-invoice, no un UUID nuevo en cada reintento.
  • El motivo es legal, no técnico: un número fiscal consumido y una factura presentada en el sistema nacional de facturación electrónica no se pueden retirar.
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" }
  }'

Todos los endpoints

MétodoEndpointScopeIdempotency-KeyPeticiones / minuto
POST/auth/token
Cambiar las credenciales por un token de acceso
——120
GET/me
Tenant, scopes y límites actuales
invoices:read—120
POST/invoices
Emitir una factura o proforma
invoices:writeobligatorio30
GET/invoices
Listar facturas (paginación por cursor)
invoices:read—120
POST/invoices/preview-totals
Calcular totales sin crear nada
invoices:read—120
GET/invoices/{invoice_id}
Obtener una factura
invoices:read—120
GET/invoices/{invoice_id}/pdf
Descargar el PDF de la factura
invoices:read—120
GET/invoices/{invoice_id}/xml
Descargar el XML UBL / CIUS-RO (Rumanía)
invoices:read—120
POST/invoices/{invoice_id}/storno
Emitir un abono que anula esta factura
invoices:writeobligatorio30
POST/invoices/{invoice_id}/payments
Marcar la factura como pagada
invoices:write—30
DELETE/invoices/{invoice_id}/payments
Marcar la factura como no pagada
invoices:write—30
GET/clients
Listar clientes
clients:read—120
POST/clients
Crear o actualizar un cliente
clients:write—30
GET/lookup/company/{tax_id}
Validar un CUI rumano en el registro de ANAF
clients:read—120
GET/series
Listar las series de numeración
invoices:read—120
POST/invoices/{invoice_id}/efactura/validate
Preflight CIUS-RO (Rumanía) — validar sin enviar
efactura:read—120
POST/invoices/{invoice_id}/efactura/submit
Enviar la factura a ANAF (SPV, Rumanía)
efactura:submitobligatorio10
GET/efactura/submissions
Listar los envíos a ANAF (Rumanía)
efactura:read—120
GET/efactura/submissions/{submission_id}
Obtener un envío
efactura:read—120
POST/efactura/submissions/{submission_id}/refresh
Consulta a ANAF (Rumanía) el estado actual
efactura:read—10
GET/efactura/submissions/{submission_id}/download
Descargar el archivo de la respuesta firmada
efactura:read—120
GET/efactura/inbox
Facturas de proveedor recibidas a través del SPV (Rumanía)
efactura:read—120
GET/efactura/inbox/{inbound_id}
Obtener una factura recibida
efactura:read—120
GET/efactura/inbox/{inbound_id}/download
Descargar el archivo de la factura recibida
efactura:read—120
POST/efactura/inbox/sync
Traer los mensajes nuevos del SPV (ANAF, Rumanía) a la bandeja
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

El circuito ANAF e-Factura (Rumanía)

not_sentvalidatingqueuedsentprocessingacceptedrejectederror
  • El verdadero diferenciador: para Rumanía, todo el ciclo de e-Factura con ANAF se realiza a través de la API, no solo la generación del documento.
  • POST /invoices/{id}/efactura/validate ejecuta, para el mercado rumano, la validación previa CIUS-RO y devuelve exactamente los campos que ANAF rechazaría, sin enviar nada.
  • GET /invoices/{id}/xml genera el XML UBL / CIUS-RO (el perfil rumano) sin presentarlo: útil para tu propio archivo o para una auditoría interna.
  • POST /invoices/{id}/efactura/submit lo presenta en el SPV (ANAF, Rumanía). Es irreversible: una segunda copia de la misma factura no se puede retirar del SPV rumano.
  • El ciclo de vida está normalizado: not_sent → validating → queued → sent → processing → accepted | rejected | error. accepted significa transmitida legalmente; error es un fallo de transporte por nuestra parte y se puede reintentar con seguridad.
  • GET /efactura/submissions/{id}/download descarga, para el mercado rumano, el archivo con la respuesta firmada de ANAF.
  • También existe el lado de recepción: POST /efactura/inbox/sync trae los mensajes nuevos del SPV (ANAF, Rumanía) y GET /efactura/inbox lista las facturas que te han enviado tus proveedores.
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)

Errores

  • Todos los 4xx y 5xx usan el mismo envoltorio: { "error": { type, code, message, param?, doc_url, request_id, errors? } }.
  • type es la clase general sobre la que ramificas (“¿puedo reintentar?”), y code es el motivo estable que mapeas en tu propio sistema.
  • param es la ruta con puntos del campo erróneo, por ejemplo lines.0.vat_rate.
  • errors[] aparece cuando una misma petición tiene problemas en más de un campo.
  • request_id queda registrado por nuestra parte. Indícalo en cualquier ticket de soporte: identifica la petición exacta.
  • También se devuelve un 404 para un id que pertenece a otro tenant: nunca confirmamos que existan datos de otra persona.
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" }
    ]
  }
}
EstadoTipoCódigo
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

Límites de peticiones

BucketPeticiones / minutoQué significa
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
  • Los límites son por credencial y por minuto: 120 para lecturas, 30 para escrituras y 10 para las operaciones de facturación electrónica (submit, refresh, inbox/sync).
  • X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset aparecen en TODAS las respuestas, incluida la 429.
  • Un 429 incluye además Retry-After en segundos. Respétalo; no reintentes en un bucle cerrado.
  • El cupo de 10/min existe porque, en Rumanía, la propia ANAF permite aproximadamente una llamada de listado de mensajes por minuto y por CIF.
  • Programa /efactura/inbox/sync en un cron, no en un sondeo agresivo.
  • ¿Necesitas límites más altos? Solicítalo a través de Contacto indicando tu volumen diario previsto.

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
// Respeta Retry-After; no reintentes nunca un 4xx distinto 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;
}

// Un reintento de escritura DEBE reutilizar el MISMO Idempotency-Key, o emitirás
// un segundo documento fiscal.

Guías por tema

Cada tema tiene su página, con sus endpoints, ejemplos y errores.

Errores frecuentes

  • Enviar el IVA como fracción (0.19) o como 1. Ambos se rechazan; envía 19 o 21.
  • Generar un token por petición. El token dura una hora; guárdalo en caché o alcanzarás el límite por IP del endpoint de tokens.
  • Generar una clave de idempotencia nueva en cada reintento: justo el escenario que produce facturas duplicadas.
  • Llamar desde JavaScript en el navegador. CORS bloquea todos los orígenes y, de todos modos, el secreto quedaría expuesto.
  • Presentar en el sistema nacional de facturación electrónica sin la validación previa (CIUS-RO en Rumanía). Un envío erróneo no se puede retirar.
  • Consultar /efactura/submissions/{id}/refresh más de 10 veces por minuto: ese cupo se comparte con submit.
  • Emitir antes de que el perfil de la empresa esté completo y exista una serie fiscal activa.

Preguntas y respuestas para desarrolladores

¿Cómo obtengo credenciales para la API de 4invoices?

En el panel, en Facturación → Ajustes → Acceso API. Recibes un client_id y un client_secret; el secreto se muestra una sola vez y se guarda solo como hash.

¿Cuánto dura el access_token?

3600 segundos, es decir una hora. Guárdalo en caché y renuévalo antes de que caduque — el endpoint de token tiene su propio límite por IP.

¿Cómo envío el tipo de IVA?

Como porcentaje: 21 para el 21%. Los valores en (0, 1] se rechazan, así que 0.19 y 1 fallan la validación.

¿Qué llamadas exigen Idempotency-Key?

POST /invoices, POST /invoices/{id}/storno y POST /invoices/{id}/efactura/submit. Las claves se recuerdan 24 horas y una repetición devuelve el documento original con Idempotency-Replayed: true.

¿Puedo enviar una factura a e-Factura de ANAF (Rumanía) directamente desde la API?

Sí, para el mercado rumano: verificación CIUS-RO con /efactura/validate, envío con /efactura/submit, seguimiento del estado normalizado en /efactura/submissions/{id} y descarga de la respuesta firmada por ANAF.

¿Cuáles son los límites de peticiones?

Por credencial y minuto: 120 lecturas, 30 escrituras y 10 operaciones de facturación electrónica. Cada respuesta trae cabeceras X-RateLimit-*, y un 429 añade Retry-After.

¿Puedo llamar a la API desde el frontend?

No. Es exclusivamente servidor a servidor: ningún origen de navegador pasa CORS y un client_secret no debe estar en código que llega al visitante.

¿Por qué recibo issuer_profile_incomplete en la primera factura?

El perfil de empresa está incompleto o no hay una serie activa de tipo fiscale. Complétalos en el panel; la API se niega a emitir un documento fiscal inválido.