API Reference
API REST para extracción automática de facturas españolas. PDF → Facturae 3.2.2 firmado con XAdES-BES en una sola llamada.
POST /convert
El endpoint más importante de la API. Acepta un PDF o un JSON con los datos de la factura y devuelve el Facturae XML 3.2.2 firmado con XAdES-BES, junto con los datos estructurados extraídos.
curl -X POST https://api.facturax.app/convert \ -H "X-API-Key: fct_tu_api_key" \ -F "file=@factura.pdf"
import requests response = requests.post( "https://api.facturax.app/convert", headers={"X-API-Key": "fct_tu_api_key"}, files={"file": open("factura.pdf", "rb")} ) data = response.json() # Guardar el XML firmado with open(data["filename"], "w") as f: f.write(data["xml"])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| vendor_name | string | requerido | Nombre o razón social del emisor |
| vendor_vat | string | requerido | CIF, NIF o NIE del emisor. Ej: B12345678 |
| vendor_address | string | opcional | Dirección completa del emisor |
| vendor_city | string | opcional | Ciudad del emisor |
| vendor_postal_code | string | opcional | Código postal (5 dígitos). Ej: 28001 |
| vendor_province | string | opcional | Provincia del emisor |
| vendor_country | string | opcional | Código ISO 3166-1 alfa-3. Por defecto: ESP |
| vendor_email | string | opcional | Email de contacto del emisor |
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| buyer_name | string | requerido | Nombre o razón social del receptor |
| buyer_vat | string | requerido | CIF, NIF o NIE del receptor |
| buyer_address | string | opcional | Dirección del receptor |
| buyer_city | string | opcional | Ciudad del receptor |
| buyer_postal_code | string | opcional | Código postal del receptor |
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| invoice_number | string | requerido | Número de factura. Ej: F2024-001 |
| invoice_date | string | requerido | Fecha emisión formato YYYY-MM-DD. Ej: 2024-06-15 |
| total | number | requerido | Importe total en euros (IVA incluido) |
| subtotal | number | opcional | Base imponible sin IVA |
| tax | number | opcional | Cuota de IVA en euros |
| declared_tax_rate | number | opcional | Tipo de IVA principal en %. Valores: 21 · 10 · 4 · 0 |
| irpf_rate | number | opcional | Porcentaje de retención IRPF. Ej: 15 |
| irpf_amount | number | opcional | Importe retención IRPF (negativo). Ej: -15.00 |
| due_date | string | opcional | Fecha de vencimiento formato YYYY-MM-DD |
| currency | string | opcional | Código ISO 4217. Por defecto: EUR |
| iban | string | opcional | IBAN del emisor. Ej: ES21 0049 0001 5021 0001 3303 |
| payment_method | string | opcional | Valores: transferencia · domiciliacion · cheque · efectivo · tarjeta |
line_items[]| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| description | string | requerido | Descripción del concepto |
| quantity | number | requerido | Cantidad. Acepta decimales. Ej: 1, 2.5 |
| unit_price | number | requerido | Precio unitario sin IVA |
| total | number | requerido | Total de la línea sin IVA (unit_price × quantity) |
| tax_rate | number | opcional | % IVA de esta línea. Ej: 21 · 10 · 4 · 0 |
{
// Emisor
"vendor_name": "Servicios Tech S.L.",
"vendor_vat": "B12345678",
"vendor_address": "Calle Mayor 1, 2º",
"vendor_city": "Madrid",
"vendor_postal_code": "28001",
"vendor_province": "Madrid",
"vendor_country": "ESP",
// Receptor
"buyer_name": "Ayuntamiento de Alcobendas",
"buyer_vat": "P2800200B",
"buyer_address": "Plaza Mayor 1",
"buyer_city": "Alcobendas",
"buyer_postal_code": "28100",
// Factura
"invoice_number": "F2024-001",
"invoice_date": "2024-06-15",
"due_date": "2024-07-15",
"currency": "EUR",
"payment_method": "transferencia",
"iban": "ES21 0049 0001 5021 0001 3303",
// Importes
"subtotal": 1000.00,
"declared_tax_rate": 21,
"tax": 210.00,
"total": 1210.00,
"irpf_rate": 15,
"irpf_amount": -150.00,
// Líneas
"line_items": [
{
"description": "Desarrollo módulo de facturación",
"quantity": 10,
"unit_price": 100.00,
"total": 1000.00,
"tax_rate": 21
}
]
}{
"vendor_name": "Tech S.L.",
"vendor_vat": "B12345678",
"buyer_name": "Ayuntamiento de Alcobendas",
"buyer_vat": "P2800200B",
"invoice_number": "F2024-001",
"invoice_date": "2024-06-15",
"total": 121.00
}curl -X POST https://api.facturax.app/convert \ -H "X-API-Key: fct_tu_api_key" \ -H "Content-Type: application/json" \ -d '{"vendor_name":"Tech S.L.","vendor_vat":"B12345678","buyer_name":"Ayto Alcobendas","buyer_vat":"P2800200B","invoice_number":"F-001","invoice_date":"2024-06-15","total":121.0}'
{
"xml": "<?xml version=\"1.0\"...>", // XML completo como string
"xml_base64": "PD94bWwg...", // XML en base64 para binario
"signed": true, // true si lleva firma XAdES-BES
"source": "pdf", // "pdf" o "json"
"invoice": {
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"invoice_number":"F2024-001",
"invoice_date": "2024-06-15",
"subtotal": 100.00,
"tax": 21.00,
"total": 121.00,
"iban": "ES21 0049...",
"accounting": { "account_code": "628", "account_label": "Suministros" }
},
"warnings": [],
"confidence": 0.97, // Solo en modo PDF
"filename": "facturae_F2024-001.xml",
"meta": {
"version": "3.2.2",
"standard": "Facturae",
"signature": "XAdES-BES"
}
}Si tu software gestiona los certificados de tus clientes, puedes pasarlos directamente en la llamada. El certificado no se almacena en nuestros servidores — se usa y descarta.
import base64, requests with open("certificado_cliente.p12", "rb") as f: cert_b64 = base64.b64encode(f.read()).decode() response = requests.post( "https://api.facturax.app/convert", headers={ "X-API-Key": "fct_tu_api_key", "X-P12-B64": cert_b64, "X-P12-Password": "contraseña_certificado", }, files={"file": open("factura.pdf", "rb")} )
⚡ Quick Start — 5 minutos
De cero a tu primera factura extraída en menos de 5 minutos. Sin configuración, sin OAuth, sin complicaciones.
/convert con el JSON de tu factura — recibirás el XML firmado con XAdES-BESPaso 2: Haz tu primera llamada. El plan gratuito incluye 2 facturas sin tarjeta.
# Extrae todos los datos de una factura PDF curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_tu_api_key" \ -F "file=@factura.pdf"
import requests API_KEY = "fct_tu_api_key" with open("factura.pdf", "rb") as f: res = requests.post( "https://api.facturax.app/extract", headers={"X-API-Key": API_KEY}, files={"file": ("factura.pdf", f, "application/pdf")}, ) data = res.json() print(data["vendor_name"], data["total"], data["confidence_score"])
import fs from 'fs'; import FormData from 'form-data'; import fetch from 'node-fetch'; const fd = new FormData(); fd.append('file', fs.createReadStream('factura.pdf'), 'factura.pdf'); const res = await fetch('https://api.facturax.app/extract', { method: 'POST', headers: { 'X-API-Key': 'fct_tu_api_key', ...fd.getHeaders() }, body: fd, }); const data = await res.json(); console.log(data.vendor_name, data.total);
Flujo completo: PDF → Facturae firmado
import requests API_KEY = "fct_tu_api_key" BASE = "https://api.facturax.app" HEADERS = {"X-API-Key": API_KEY} # Paso 1 — Extraer datos del PDF with open("factura.pdf", "rb") as f: r1 = requests.post(f"{BASE}/extract", headers=HEADERS, files={"file": ("factura.pdf", f, "application/pdf")}) r1.raise_for_status() invoice_data = r1.json() # Comprobar confianza antes de continuar if invoice_data["confidence_score"] < 0.7: print("Confianza baja — revisar manualmente", invoice_data["warnings"]) # Paso 2 — Generar y descargar Facturae XML firmado r2 = requests.post(f"{BASE}/convert-to-facturae/download", headers={**HEADERS, "Content-Type": "application/json"}, json={"invoice_data": invoice_data, "sign": True}) r2.raise_for_status() with open("factura_firmada.xml", "wb") as out: out.write(r2.content) print("✓ Facturae firmado guardado en factura_firmada.xml")
Códigos de error
Todos los errores devuelven JSON con error_code (legible por máquina) y detail (legible por humano). Usa error_code para la lógica de reintento — nunca parsees detail.
{
"error_code": "QUOTA_EXCEEDED", // código máquina-legible — estable entre versiones
"detail": "Cuota agotada. 150/150 usadas este mes." // mensaje humano — puede cambiar
}
RETRYABLE = {"EXTRACTION_FAILED", "SERVICE_ERROR", "XML_BUILD_FAILED"}
def handle_error(response):
err = response.json()
code = err.get("error_code", "UNKNOWN")
if code in RETRYABLE:
retry_with_backoff() # error transitorio — reintentar
elif code == "QUOTA_EXCEEDED":
alert_team("Añadir créditos") # no reintentar — acción humana
elif code == "RATE_LIMIT_EXCEEDED":
wait = response.headers.get("Retry-After", 60)
time.sleep(int(wait)) # esperar Retry-After y reintentar
elif code in {"AUTH_INVALID_KEY", "AUTH_REQUIRED"}:
raise ConfigError("Revisar API key") # nunca reintentar
Catálogo completo de error_code
| error_code | HTTP | ¿Reintentar? | Causa y acción |
|---|---|---|---|
| Autenticación | |||
| AUTH_REQUIRED | 401 | No | Falta la API key o el JWT. Añade X-API-Key o Authorization: Bearer. |
| AUTH_INVALID_KEY | 401 | No | API key incorrecta, revocada o inexistente. Verifica en el dashboard. |
| AUTH_INVALID_TOKEN | 401 | No | JWT expirado o manipulado. Re-autentícate con POST /auth/login. |
| ACCESS_DENIED | 403 | No | Email no verificado, cuenta suspendida o recurso de otro usuario. |
| Cuota y rate limit | |||
| QUOTA_EXCEEDED | 429 | No | Plan mensual agotado o créditos API a cero. Añade créditos o espera al reset mensual. |
| RATE_LIMIT_EXCEEDED | 429 | Sí | Demasiadas peticiones por minuto. Espera el valor de Retry-After y reintenta. |
| PLAN_REQUIRED | 403 | No | La función requiere un plan superior (ej: webhooks en Pro, subaccounts en Business). |
| BATCH_NOT_AVAILABLE | 403 | No | El lote requiere plan Starter o superior. |
| BATCH_LIMIT_EXCEEDED | 400 | No | El lote supera el máximo de archivos permitido por el plan. |
| Archivo | |||
| FILE_TYPE_INVALID | 400 | No | Formato no soportado. Acepta PDF, PNG, JPG y WebP. |
| FILE_CONTENT_MISMATCH | 400 | No | La extensión no coincide con el contenido real del archivo (magic bytes). Verifica que el archivo no está corrupto o renombrado. |
| FILE_TOO_LARGE | 413 | No | Archivo superior a 20 MB. Comprime o divide antes de subir. |
| Extracción y generación XML | |||
| EXTRACTION_FAILED | 500 | Sí | Error transitorio en el OCR o en GPT. Reintenta con backoff exponencial (máx 3 veces). |
| XML_BUILD_FAILED | 500 | Sí | Error construyendo el XML Facturae. Transitorio — reintenta. Si persiste, contacta soporte. |
| XML_SIGN_FAILED | 500 | No | Error firmando con XAdES-BES. Verifica que el certificado .p12 es válido y la contraseña correcta. |
| SERVICE_ERROR | 500 | Sí | Error interno genérico. Siempre transitorio — reintenta. Si persiste más de 5 min, consulta status.facturax.app. |
| Certificado | |||
| CERT_INVALID_FORMAT | 400 | No | El archivo no es un .p12/.pfx válido. |
| CERT_INVALID_PASSWORD | 400 | No | Contraseña del .p12 incorrecta. |
| CERT_TOO_LARGE | 400 | No | Certificado superior a 5 MB. |
| CERT_NOT_FOUND | 404 | No | El certificate_id no existe o pertenece a otro usuario. |
| Recursos | |||
| INVOICE_NOT_FOUND | 404 | No | El log_id no existe o fue creado con otra API key. |
| JOB_NOT_FOUND | 404 | No | El job_id del lote no existe o ya expiró (TTL 24h). |
| WEBHOOK_NOT_FOUND | 404 | No | El webhook no existe o pertenece a otro usuario. |
| KEY_NOT_FOUND | 404 | No | La API key no existe o ya fue revocada. |
| Input inválido | |||
| INVALID_INPUT | 400 | No | Parámetro obligatorio ausente o formato incorrecto. El campo detail especifica cuál. |
| INVALID_EMAIL | 400 | No | Email con formato inválido en el registro. |
| DISPOSABLE_EMAIL | 400 | No | El dominio del email es temporal o desechable. |
| INVALID_PLAN | 400 | No | Plan desconocido en el checkout. Valores válidos: starter, pro. |
| WEBHOOK_INVALID_EVENT | 400 | No | Evento de webhook desconocido. Válidos: invoice.extracted, invoice.signed, quota.warning, *. |
| WEBHOOK_INVALID_URL | 400 | No | La URL del webhook no es HTTPS o no responde a un OPTIONS. |
| Pagos | |||
| STRIPE_NOT_CONFIGURED | 503 | Sí | El sistema de pagos está temporalmente no disponible. Reintenta en unos minutos. |
| STRIPE_ERROR | 500 | Sí | Error en la pasarela de pago. Transitorio — reintenta o contacta soporte. |
/extract incluye X-Quota-Used, X-Quota-Limit y X-Quota-Remaining para monitorizar el consumo sin llamadas adicionales.Rate limit: 60 req/min por API key (independiente de la IP). La cabecera
Retry-After indica los segundos de espera. La cabecera X-RateLimit-Plan indica el plan que está limitando.Idempotency: Añade
X-Idempotency-Key: <uuid> para reintentar sin duplicar consumo. Durante 24h, la misma key devuelve la respuesta cacheada sin consumir cuota. En replay, la respuesta incluye X-Idempotency-Replayed: true.500 sin error_code: Si recibes un 500 sin campo
error_code, es un error no anticipado. Trata estos como SERVICE_ERROR (reintenta hasta 3 veces con backoff) y reporta a soporte@facturax.app con el X-Request-ID de la cabecera de respuesta.
Rate limits
Las peticiones se limitan por API key, no por IP. Todos los planes autenticados comparten el mismo límite base. Las peticiones OPTIONS no cuentan.
Si mandas facturas en batch (decenas o cientos de golpe), añade un delay de 1 segundo entre llamadas o usa el endpoint POST /batch que procesa múltiples facturas en una sola petición y no tiene este límite por factura. Sin delay, las peticiones que superen 60/min recibirán un 429 con el header Retry-After.
import requests, time def convert_with_retry(invoice_data, api_key, max_retries=3): headers = {"X-API-Key": api_key, "Content-Type": "application/json"} for attempt in range(max_retries): r = requests.post("https://api.facturax.app/convert", headers=headers, json=invoice_data) if r.status_code == 429: wait = int(r.headers.get("Retry-After", 60)) print(f"Rate limit — esperando {wait}s") time.sleep(wait) continue r.raise_for_status() return r.json() raise Exception("Rate limit persistente tras 3 reintentos") # Para batches: procesar con delay entre llamadas facturas = [...] # lista de dicts con datos de factura for factura in facturas: resultado = convert_with_retry(factura, "fct_tu_api_key") time.sleep(1) # 1s entre llamadas → máx 60/min, nunca toca el límite
Si necesitas procesar más de 60 facturas/minuto de forma sostenida, contacta en soporte@facturax.app — los planes Business tienen límites personalizados.
Casos de uso
Flujos reales de integración para los casos más habituales.
1. Gestoría — procesamiento automático de emails
Recibir facturas por email, subirlas a FacturaX, obtener el JSON y el Facturae firmado, y registrarlas en el software contable.
import requests def procesar_factura(pdf_path: str) -> dict: # 1. Extraer datos with open(pdf_path, "rb") as f: res = requests.post( "https://api.facturax.app/extract", headers={"X-API-Key": API_KEY}, files={"file": (pdf_path, f, "application/pdf")} ) data = res.json() # 2. Alertar si confianza baja o fraude detectado if data.get("confidence_score", 1) < 0.75: alert_revisar(pdf_path, data["warnings"]) fraud = data.get("fraud_checks", {}) if fraud.get("iban_check", {}).get("detected"): alert_fraude_iban(data["vendor_name"], data["iban"]) # 3. Cuenta PGC asignada automáticamente cuenta = data["accounting"]["account_code"] # ej: "628" # 4. Generar Facturae firmado si es para AAPP if es_para_administracion(data["buyer_vat"]): r2 = requests.post( "https://api.facturax.app/convert-to-facturae/download", headers={"X-API-Key": API_KEY}, json={"invoice_data": data, "sign": True} ) with open(pdf_path.replace(".pdf", ".xml"), "wb") as out: out.write(r2.content) return data
2. Procesamiento masivo con batch jobs (Starter+)
import requests, time, glob files = glob.glob("facturas/*.pdf")[:50] handles = [("files", (open(f, "rb"))) for f in files] # Crear job en background — responde inmediatamente res = requests.post("https://api.facturax.app/batch/job", headers={"X-API-Key": API_KEY}, files=handles) job_id = res.json()["job_id"] # Polling hasta completar while True: status = requests.get(f"https://api.facturax.app/batch/job/{job_id}", headers={"X-API-Key": API_KEY}).json() print(f"{status['processed']}/{status['total']} procesadas") if status["status"] == "done": break time.sleep(3)
3. Webhook — notificación en tiempo real
from flask import Flask, request import hmac, hashlib app = Flask(__name__) WEBHOOK_SECRET = "tu_webhook_secret" @app.route("/webhook", methods=["POST"]) def webhook(): sig = request.headers.get("X-Factura-Signature", "") expected = "sha256=" + hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): return "Firma inválida", 401 data = request.json() # Procesar evento print(data["event"], data["data"]["vendor_name"]) return "", 200
Autenticación
Todas las peticiones deben incluir una API key en la cabecera X-API-Key o un JWT en Authorization: Bearer <token>. Las API keys se crean desde el dashboard o via POST /keys.
X-API-Key: fct_xxxxxxxxxxxx. Ideal para server-to-server. Disponible desde el plan Pro.JWT (para el dashboard): Obtén un token con
POST /auth/login e inclúyelo como Authorization: Bearer <token>. Validez de 7 días.
La API acepta peticiones desde cualquier dominio — puedes llamarla directamente desde tu frontend, tu ERP web, o cualquier aplicación sin configuración adicional. La seguridad la proporciona la API key, no el origen de la petición.
Si ves un error CORS o Failed to fetch en tus primeras pruebas, asegúrate de que estás incluyendo el header X-API-Key correctamente — no es un problema de origen sino de autenticación.
# Con API Key curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_tu_api_key" \ -F "[email protected]" # Con JWT curl -X POST https://api.facturax.app/extract \ -H "Authorization: Bearer eyJhbGci..." \ -F "[email protected]"
Body (JSON)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| string | requerido | Email de la cuenta | |
| password | string | requerido | Contraseña (mínimo 8 caracteres) |
Respuesta
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": 1,
"email": "[email protected]",
"full_name": "Juan García",
"plan": "pro"
}
}
Códigos de respuesta
Extracción de facturas
El endpoint principal. Acepta una factura en PDF, PNG, JPG o WebP y devuelve todos los campos extraídos en JSON, junto con verificaciones antifraude (Pro+) y categorización contable PGC automática.
— ≥ 0.9: extracción completa, puedes procesar automáticamente.
— 0.7 – 0.9: extracción parcial, revisa los
warnings.— < 0.7: revisión manual recomendada. La factura puede estar mal escaneada o en un formato inusual.
Cabeceras de cuota: Cada respuesta incluye
X-Quota-Used, X-Quota-Limit y X-Quota-Remaining.
Request — multipart/form-data
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| file | file | requerido | Factura en PDF, PNG, JPG o WebP. Máximo 20MB. Soporta multipágina y escaneados. |
Ejemplos
curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_tu_api_key" \ -F "file=@factura.pdf"
import requests with open("factura.pdf", "rb") as f: res = requests.post( "https://api.facturax.app/extract", headers={"X-API-Key": "fct_tu_api_key"}, files={"file": ("factura.pdf", f, "application/pdf")}, ) data = res.json() # Comprobar confianza if data["confidence_score"] >= 0.9: procesar_automaticamente(data) else: enviar_a_revision(data, data["warnings"])
const fd = new FormData(); fd.append('file', file); // file es un objeto File del input const res = await fetch('https://api.facturax.app/extract', { method: 'POST', headers: { 'X-API-Key': 'fct_tu_api_key' }, body: fd }); const data = await res.json(); console.log(`Cuota: ${res.headers.get('X-Quota-Remaining')} restantes`);
Respuesta
{
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"vendor_address": "C/ Hermosilla 3",
"vendor_city": "Madrid",
"buyer_name": "Mi Empresa S.L.",
"buyer_vat": "B12345678",
"invoice_number": "FAC-2024-0312",
"invoice_date": "2024-03-15",
"due_date": "2024-04-15",
"currency": "EUR",
"subtotal": 154.90,
"tax": 32.53,
"total": 187.43,
"iban": "ES7621000813610123456789",
"payment_method": "transferencia",
"irpf_rate": null,
"declared_tax_rate": 21,
"line_items": [
{
"description": "Consumo eléctrico marzo",
"quantity": 1,
"unit_price": 154.90,
"tax_rate": 21,
"total": 154.90
}
],
"confidence_score": 0.97,
"warnings": [],
"accounting": {
"account_code": "628",
"account_label": "Suministros",
"type": "gasto",
"type_label": "Gasto del ejercicio",
"confidence": "alta",
"method": "regla",
"reasoning": "Iberdrola es proveedor de suministros eléctricos."
},
"fraud_checks": {
"duplicate": { "detected": false },
"vat_verification": { "valid": true, "status": "verified" },
"iban_check": { "detected": false }
},
"_log_id": 42
}
// Cabeceras de respuesta:
// X-Quota-Used: 3
// X-Quota-Limit: 150
// X-Quota-Remaining: 147
Códigos de respuesta
Query params
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| limit | integer | opcional | Registros por página, 1-200 (por defecto 50) |
| offset | integer | opcional | Registros a saltar para paginar (por defecto 0) |
| q | string | opcional | Búsqueda de texto libre en filename, emisor, nº factura y external_ref |
| status | string | opcional | ok o error para filtrar por resultado |
| date_from | string | opcional | Fecha inicio en formato YYYY-MM-DD |
| date_to | string | opcional | Fecha fin en formato YYYY-MM-DD |
| subaccount_id | integer | opcional | Filtrar por ID de subcuenta (solo plan Business) |
Códigos de respuesta
{
"total": 87,
"avg_confidence": 0.924,
"total_errors": 2,
"total_with_warnings": 11,
"quota": 150,
"used": 87,
"remaining": 63,
"plan": "starter",
"quota_reset_at": "2024-04-15 08:00:00",
"daily_usage": [
{ "day": "2024-03-15", "count": 12 }
]
}
Forma rápida de consultar la cuota sin llamar a /metrics. Útil para mostrar en dashboards o decidir si lanzar un batch.
{
"plan": "starter",
"quota": 150,
"used": 87,
"remaining": 63,
"quota_reset_at": "2024-04-15 08:00:00",
"pct_used": 58.0
}
Batch Jobs
Procesa múltiples facturas en background. El job se crea y devuelve job_id inmediatamente — no tienes que esperar. Disponible desde el plan Starter. Recibirás un email cuando termine.
El polling recomendado es cada 3-5 segundos. No hagas polling más rápido para no desperdiciar cuota de rate limit.
Request — multipart/form-data
| Campo | Tipo | Descripción |
|---|---|---|
| files | file[] | Array de facturas PDF/PNG/JPG/WebP. Máx según plan. |
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"total": 25,
"status": "running",
"message": "Procesando 25 facturas en background. Recibirás un email al terminar."
}
{
"job_id": "550e8400-...",
"status": "done",
"total": 25,
"processed": 25,
"failed": 1,
"created_at": "2024-03-15 10:00:00",
"finished_at":"2024-03-15 10:01:43",
"items": [
{ "filename": "factura1.pdf", "status": "ok", "result": { ... } },
{ "filename": "factura2.pdf", "status": "error", "result": { "error": "..." } }
]
}
Devuelve los últimos 20 batch jobs ordenados por fecha descendente, sin los items individuales (para eso usa GET /batch/job/{job_id}).
Request — multipart/form-data
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| file | file | requerido | Factura en PDF, PNG o JPG. Máximo 20MB. Soporta multipágina y escaneados. |
Ejemplo
curl -X POST https://api.facturax.app/extract \ -H "X-API-Key: fct_tu_api_key" \ -F "[email protected]"
const fd = new FormData(); fd.append('file', file); const res = await fetch('https://api.facturax.app/extract', { method: 'POST', headers: { 'X-API-Key': 'fct_tu_api_key' }, body: fd }); const data = await res.json();
Respuesta
{
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"invoice_number": "FAC-2024-0312",
"invoice_date": "2024-03-15",
"due_date": "2024-04-15",
"currency": "EUR",
"subtotal": 154.90,
"tax": 32.53,
"total": 187.43,
"iban": "ES7621000813610123456789",
"line_items": [
{
"description": "Consumo eléctrico marzo",
"quantity": 1,
"unit_price": 154.90,
"total": 187.43
}
],
"confidence_score": 0.97,
"warnings": [],
"accounting": {
"account_code": "628",
"account_label": "Suministros",
"type": "gasto",
"confidence": "alta"
},
"fraud_checks": {
"duplicate": { "detected": false },
"vat_verification": { "valid": true, "status": "verified" },
"iban_check": { "detected": false }
},
"_log_id": 42
}
Códigos de respuesta
Query params
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| limit | integer | opcional | Número de resultados (por defecto 50, máx. 100) |
Códigos de respuesta
Firmar XML Facturae
Endpoint para ERPs que ya generan su propio XML Facturae pero no tienen firma XAdES-BES integrada. Envía el XML sin firmar y devuelve el .xml listo para subir a FACe. 1 crédito por llamada.
Prueba el firmador directamente en el navegador — sube tu .p12, selecciona un XML Facturae y descarga el resultado firmado.
Si tu ERP ya genera el XML Facturae pero no firma con XAdES-BES, este es tu endpoint. No necesitas cambiar cómo generas las facturas — solo añades un POST y recibes el .xml firmado con el certificado FNMT del usuario.
curl -X POST https://api.facturax.app/sign \ -H "X-API-Key: fct_tu_api_key" \ -H "X-Certificate-ID: cert_abc123" \ -H "X-External-Ref: F2024-001" \ -F "file=@factura_sin_firmar.xml"
curl -X POST https://api.facturax.app/sign \ -H "X-API-Key: fct_tu_api_key" \ -H "X-External-Ref: F2024-001" \ -F "file=@factura_sin_firmar.xml"
import requests # El XML lo genera tu ERP (STEL Order, Selenne, NCS, etc.) # El certificate_id se obtiene una vez con POST /certificates with open("factura_sin_firmar.xml", "rb") as f: r = requests.post( "https://api.facturax.app/sign", headers={ "X-API-Key": "fct_tu_api_key", "X-Certificate-ID": "cert_abc123", # certificado del cliente "X-External-Ref": "F2024-001", }, files={"file": ("factura.xml", f, "application/xml")}, ) data = r.json() with open(data["filename"], "w", encoding="utf-8") as out: out.write(data["xml"]) print(f"Firmado: {data['filename']}") # .xml listo para FACe
{
"xml": "<?xml version="1.0"...><ds:Signature...>", // XML firmado con XAdES-BES
"xml_base64": "PD94bWwgdmVyc2lvb...", // Base64 del .xml
"signed": true,
"source": "sign",
"log_id": 1234,
"filename": "factura_sin_firmar.xml"
}X-Certificate-ID > X-P12-B64 > certificado del perfil. Para ERPs multi-cliente lo ideal es usar POST /certificates una vez por cliente y referenciar con X-Certificate-ID en cada llamada.Validaciones automáticas: Rechaza XMLs que no sean Facturae (
INVALID_INPUT), XMLs que ya tengan firma (INVALID_INPUT), y certificate_id inexistente o de otra cuenta (CERT_NOT_FOUND).
Facturae XML 3.2.2
Convierte el JSON extraído al formato Facturae 3.2.2, el estándar oficial español para facturación electrónica requerido por la Ley Crea y Crece. Compatible con FACe y la Administración Pública.
Body (JSON)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| invoice_data | object | requerido | JSON del resultado de /extract |
| seller_info | object | opcional | Datos adicionales del emisor: name, vat, address, city, zip |
| buyer_info | object | opcional | Datos del receptor: name, vat, address, city, zip |
| sign | boolean | opcional | Firmar con XAdES-BES. Requiere certificate_id, p12_b64 o certificado personal guardado en la cuenta. |
| certificate_id | string | opcional | ID de un certificado gestionado (de POST /certificates). Recomendado para ERPs — evita enviar el .p12 en cada petición. Prioridad sobre p12_b64. |
| p12_b64 | string | opcional | Certificado .p12 en base64 para firma puntual. Para integraciones masivas usa certificate_id en su lugar. |
Respuesta
{
"xml": "<?xml version=\"1.0\"?><fe:Facturae ...>",
"version": "3.2.2",
"signed": false,
"filename": "facturae_FAC-2024-0312.xml"
}
Mismo body que /convert-to-facturae pero devuelve el archivo XML directamente con Content-Disposition: attachment.
curl -X POST https://api.facturax.app/convert-to-facturae/download \ -H "X-API-Key: fct_tu_api_key" \ -H "Content-Type: application/json" \ -d '{"invoice_data": {...}}' \ -o factura.xml
Antifraude
Gestiona los IBANs de confianza de tus proveedores y confirma o descarta alertas de duplicados. Las verificaciones antifraude se ejecutan automáticamente en cada extracción.
Respuesta
{
"vendors": [
{
"vendor_vat": "A95758389",
"trusted_iban": "ES7621000813610123456789",
"last_seen": "2024-03-15 10:32:00"
}
]
}
Path params
| Campo | Tipo | Descripción |
|---|---|---|
| vendor_vat | string | CIF/NIF del proveedor (ej: A95758389) |
Body (JSON)
| Campo | Tipo | Descripción |
|---|---|---|
| iban | string | Nuevo IBAN de confianza (sin espacios) |
Categorización contable
Asigna automáticamente la cuenta del Plan General Contable español usando reglas deterministas + IA con el PGC completo embebido.
Body (JSON)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| vendor_name | string | opcional | Nombre del emisor |
| vendor_vat | string | opcional | CIF/NIF del emisor |
| line_items | array | opcional | Líneas de la factura con description |
| total | number | opcional | Importe total (influye en gasto vs inversión) |
Respuesta
{
"account_code": "628",
"account_label": "Suministros",
"type": "gasto",
"type_label": "Gasto del ejercicio",
"reasoning": "Iberdrola es un proveedor de suministros eléctricos.",
"confidence": "alta",
"method": "regla",
"suggestions": []
}
API Keys
Gestiona las API keys para integraciones externas. Disponible en planes Pro y Business.
Body (JSON)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | requerido | Nombre descriptivo (ej: "Mi ERP") |
| plan | string | opcional | "free" o "pro". Por defecto "free". |
Webhooks — Notificaciones en tiempo real
En lugar de hacer polling cada X segundos preguntando "¿ya está mi factura?", configura un webhook y FacturaX avisará a tu servidor en el momento en que termine el procesamiento. Disponible en el plan Pro y superior.
Tu servidor → GET /status → espera
Tu servidor → GET /status → listo
FacturaX → POST tuservidor.com/hook
Tu servidor recibe el resultado ✓
X-Factura-Signature: sha256=HASH. Verifica que el hash coincide con HMAC-SHA256(payload, tu_secret) para confirmar que la petición es auténtica.
const crypto = require('crypto'); function verifyWebhook(payload, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); }
Body (JSON)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| url | string | requerido | URL HTTPS donde recibirás los eventos |
| events | array | opcional | ["*"] para todos, o uno o varios de: "invoice.extracted", "invoice.signed", "quota.warning". Por defecto ["*"]. |
| secret | string | opcional | Secret para firmar con HMAC-SHA256. Si no se provee, se genera automáticamente. |
Payload del webhook
{
"event": "invoice.extracted",
"timestamp": 1710500000,
"data": {
"log_id": 42,
"external_ref": "ERP-INV-2025-0312",
"filename": "factura.pdf",
"vendor_name": "Iberdrola S.A.U.",
"vendor_vat": "A95758389",
"invoice_number": "FAC-2024-0312",
"total": 187.43,
"confidence": 0.97,
"accounting": { "account_code": "628" },
"fraud_checks": { "duplicate": { "detected": false } }
}
}
{
"event": "invoice.signed",
"timestamp": 1710500003,
"data": {
"log_id": 42,
"external_ref": "ERP-INV-2025-0312",
"b2_file_id": "4_z1a2b3c4...",
"invoice_number": "FAC-2024-0312",
"invoice_date": "2024-03-12",
"expires_at": "2028-03-12",
"download_endpoint": "/invoices/42/facturae"
}
}
{
"event": "quota.warning",
"timestamp": 1710500010,
"data": {
"used": 128,
"quota": 150,
"remaining": 22,
"pct_remaining": 14.7,
"plan": "starter",
"message": "Te quedan solo 22 facturas (14.7% de tu saldo)."
}
}
download_endpoint para descargarlo sin polling.quota.warning dispara al 15% restante de facturas para que el ERP pueda alertar antes de quedarse sin saldo.
Códigos de respuesta
Certificados gestionados
Para ERPs con múltiples clientes. Sube el .p12 de cada empresa una sola vez y obtén un certificate_id. Úsalo en /convert-to-facturae sin reenviar el archivo en cada petición. Disponible en planes Pro y Business.
POST /certificates (una vez por cliente) → guarda el certificate_id → pásalo en cada POST /convert-to-facturae/download como {"certificate_id": "cert_...", "sign": true}
Multipart form-data. El raw_key del certificado no se puede recuperar después — almacena el certificate_id.
Form fields
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| file | file | requerido | Archivo .p12 o .pfx del cliente |
| name | string | requerido | Nombre descriptivo, ej: "Acme S.L. — FNMT 2025" |
| password | string | opcional | Contraseña del .p12 (se cifra con Fernet antes de guardar) |
# 1. Subir el .p12 del cliente una sola vez with open("acme_fnmt.p12", "rb") as f: r = requests.post(f"{BASE_URL}/certificates", headers=HEADERS, files={"file": f}, data={"name": "Acme S.L. — FNMT 2025", "password": "mi_password"} ) cert = r.json() cert_id = cert["certificate_id"] # "cert_a1b2c3d4..." — guardar en tu BD # 2. En cada factura, solo enviar el JSON + certificate_id r = requests.post(f"{BASE_URL}/convert-to-facturae/download", headers=HEADERS, json={ "invoice_data": { # ... datos de la factura }, "sign": True, "certificate_id": cert_id, # sin reenviar el .p12 "external_ref": "ACME-2025-001", } )
certificate_id, subject y expires_at del certificado.CERT_INVALID_FORMAT / CERT_INVALID_PASSWORD / CERT_TOO_LARGEPLAN_REQUIRED — solo Pro y BusinessDevuelve todos los certificados activos con su id, name, subject y expires_at. No devuelve el .p12.
Soft delete — el certificado queda inactivo. Las facturas firmadas previamente no se ven afectadas.
Subaccounts
Multi-tenant para ERPs y agencias. Crea subcuentas independientes por empresa cliente — cuota, historial y API keys aislados. Disponible en el plan Business.
X-External-Ref → consultar historial y métricas por subcuenta desde el dashboard o la API.Uso agregado, facturas 30 días y alertas automáticas al 90% de cuota.
{
"count": 3, "total_used": 87, "total_quota": 450, "pct_used": 19.3,
"alerts": [{ "subaccount": "Acme S.L.", "slug": "acme-sl", "pct": 94.0 }],
"subaccounts": [{ "id": 1, "name": "Acme S.L.", "total_used": 47, "total_quota": 50 }]
}
# 1. Crear subcuenta sub = requests.post(f"{BASE_URL}/subaccounts", headers=HEADERS, json={"name": "Acme S.L.", "notes": "CIF B12345678"}).json() # 2. Crear API key con cuota independiente key = requests.post(f"{BASE_URL}/subaccounts/{sub['id']}/keys", headers=HEADERS, json={"name": "Acme — Prod", "quota": 150}).json() acme_key = key["raw_key"] # guardar — solo se devuelve una vez # 3. Extraer con la key de Acme + su ID de factura requests.post(f"{BASE_URL}/extract", headers={"X-API-Key": acme_key, "X-External-Ref": "ACME-2025-001"}, files={"file": open("factura.pdf", "rb")}) # 4. Consultar historial de Acme con filtros hist = requests.get(f"{BASE_URL}/subaccounts/{sub['id']}/history", headers=HEADERS, params={"q": "Iberdrola", "date_from": "2025-05-01", "status": "ok"}).json()
| Campo | Tipo | Descripción |
|---|---|---|
| q | string | Texto libre: filename, emisor, nº factura, external_ref |
| status | string | ok o error |
| date_from / date_to | string | YYYY-MM-DD — rango de fechas |
| limit / offset | integer | Paginación (máx 200/página) |
{ "total": 47, "avg_confidence": 0.934, "total_errors": 1,
"keys": [{ "name": "Acme — Prod", "quota": 150, "used": 47, "remaining": 103 }],
"daily_usage": [{ "day": "2025-05-01", "count": 8 }],
"top_vendors": [{ "vendor": "Iberdrola S.A.U.", "count": 12 }] }