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.
- 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
- Genera una credencial en el panel, en Facturación → Ajustes → Acceso API, y copia el client_secret — solo se muestra una vez.
- 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.
- Genera una credencial en el panel, en Facturación → Ajustes → Acceso API, y copia el client_secret — solo se muestra una vez.
- 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.
- 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.
- Emite la factura: POST /v1/invoices con la cabecera Idempotency-Key y vat_rate como porcentaje (21, no 0.21).
- 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.
- 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étodo | Endpoint | Scope | Idempotency-Key | Peticiones / 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:write | obligatorio | 30 |
| 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:write | obligatorio | 30 |
| 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:submit | obligatorio | 10 |
| 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)
- 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" }
]
}
}| Estado | Tipo | Código |
|---|---|---|
| 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 |
Límites de peticiones
| Bucket | Peticiones / minuto | Qué significa |
|---|---|---|
| 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 |
- 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.
- Autenticación en la API de 4invoicesCambia client_id y client_secret por un token Bearer de una hora, solo de servidor a servidor.
- Emitir facturas por la APIPOST /invoices con Idempotency-Key, IVA en porcentaje, además de listado, rectificativa y marcar como pagada.
- Clientes y consulta al registro de ANAF (Rumanía)Un alta o actualización deduplicada por tax_id y una consulta al registro de ANAF para empresas rumanas, para que la e-Factura no sea rechazada.
- e-Factura de ANAF (Rumanía) por la APIPara Rumanía: verificación CIUS-RO, envío al SPV, estado normalizado, respuesta firmada y bandeja de facturas recibidas.
- Series de numeración en la APISin una serie activa de tipo fiscale, la emisión devuelve 409 no_series_configured.
- Códigos de error y la envoltura de errorUna sola forma en todas las rutas: type, code, message, param, doc_url, request_id y errors.
- Límites de peticiones y reintentos120 lecturas, 30 escrituras y 10 operaciones de facturación electrónica por minuto y credencial, con cabeceras X-RateLimit-*.
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.