Para desarrolladores

La API de Factuza

El mismo motor que usan la web y la app: huella encadenada SHA-256, numeración por serie, PDF con QR de cotejo y remisión a la AEAT.

Estado: preview para integradores. La API funciona y está probada, y los precios y el reparto de responsabilidades están en la página del Motor API. El alta todavía no tiene botón: se hace hablando. Escríbenos a hola@factuza.com y te damos acceso al entorno de pruebas el mismo día.

Esto no es una lista de intenciones: cada ejemplo de esta página se ejecuta en cada pasada de la suite (caso CU-K04). Si algo dejara de funcionar, la prueba se pone roja antes de que lo descubras tú.

1. Para quién es esto

Para software que ya gestiona un negocio —un taller, una clínica, una academia, un ERP vertical— y necesita que sus facturas cumplan VeriFactu sin reescribir la parte fiscal. Tu sistema sigue siendo el que manda; Factuza pone la numeración, la huella encadenada, el PDF y la remisión.

Lo que no es: una pasarela de firma ni un simple generador de PDF. Cada factura que emites por aquí nace con su registro de facturación, entra en la cadena de huellas y se remite a la Agencia Tributaria igual que si la hubieras emitido desde la web.

Una factura emitida no se borra. Entra en la cadena de huellas y ahí se queda. Lo que existe es anular y rectificar, que son dos operaciones distintas y las dos dejan rastro. Prueba en el entorno de pruebas antes de emitir de verdad.

2. Autenticación: la clave de API

Toda la API acepta dos credenciales: el token de una persona (lo que usan la web y la app) o una clave de API, que es la de una máquina. Para integrar, la tuya es la segunda.

Una clave se crea desde el área de clientes, en Claves de API, con tu usuario de administrador, y viaja en una cabecera:

X-Api-Key: fzk_x8Kq2vN...

Al crearla eliges qué puede hacer, y la diferencia importa:

Si tu integración no incorpora clientes, usa la primera: una clave que puede menos hace menos daño el día que se filtre. Y no se puede ampliar sobre la marcha — se crea otra a propósito.

Tres reglas que conviene saber antes

La clave hereda los permisos de tu cuenta: sus emisores, su plan y su licencia. No puede emitir a nombre de un NIF que no sea el tuyo, y si tu licencia caduca deja de emitir igual que la web — pero sigue pudiendo consultar y descargar lo ya emitido, porque tus libros son tuyos.

3. Tu primera factura, paso a paso

Cinco llamadas. Todas con la misma cabecera y ninguna con token de usuario. El ejemplo usa el entorno de pruebas, donde nada cuenta ni se remite a la AEAT.

01¿Quién soy?

Antes de nada, con qué emisores puede trabajar tu clave. De aquí sale el NIF y el nombre que van en la factura: no los teclees, léelos.

curl https://func-factuza-test-obt1.azurewebsites.net/api/emisores \
  -H "X-Api-Key: $FACTUZA_CLAVE"

{ "emisores": [ { "emisorId": 2, "nif": "B87654323",
                 "nombreRazon": "Pruebas Automaticas SL" } ],
  "plan": "PYME", "tope": 1, "usados": 1, "disponibles": 0 }

02Tu serie

El número de factura lo pone el servidor, dentro de la misma transacción que la emite. Tú mandas el serieId, no el número: teclearlo desde fuera deja la correlatividad en manos de que nadie se equivoque, y un hueco en la numeración hay que justificarlo ante Hacienda.

curl https://func-factuza-test-obt1.azurewebsites.net/api/series \
  -H "X-Api-Key: $FACTUZA_CLAVE"

{ "series": [ { "serieId": 1, "codigo": "PRU", "activa": true,
                "siguienteNumero": "PRU-2026/0457" } ], "total": 1 }

03Emitir

El cuerpo mínimo de una factura normal (F1) con una sola base y un solo tipo. Para varias líneas o tratamientos especiales de IVA, usa POST /facturas/detallada.

curl -X POST https://func-factuza-test-obt1.azurewebsites.net/api/facturas \
  -H "X-Api-Key: $FACTUZA_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "nifEmisor": "B87654323",
    "nombreEmisor": "Pruebas Automaticas SL",
    "numSerieFactura": "",          // lo pone la serie
    "serieId": 1,
    "fechaExpedicion": "2026-08-23",
    "tipoFactura": "F1",
    "descripcion": "Primera factura por API",
    "nifDestinatario": "B12345674",
    "nombreDestinatario": "Cliente de pruebas SL",
    "baseImponible": 100,
    "tipoImpositivo": 21
  }'

Responde 201 —se ha creado un recurso, no es un 200— con el número que ha puesto la serie y la huella:

{ "facturaId": 457,
  "numSerieFactura": "PRU-2026/0457",
  "huella": "9F2A…64 caracteres en hexadecimal" }

Sin huella no es VeriFactu. Son 64 caracteres: el SHA-256 del registro, encadenado con el de la factura anterior.

04El PDF

Con su QR de cotejo, listo para mandárselo al cliente.

curl https://func-factuza-test-obt1.azurewebsites.net/api/facturas/457/pdf \
  -H "X-Api-Key: $FACTUZA_CLAVE" -o factura.pdf

05Cómo vas de consumo

Esta ruta la puede llamar una máquina, al revés que las de gestión de claves: negarte saber por dónde vas y luego cobrarte el exceso sería tenderte una trampa.

curl https://func-factuza-test-obt1.azurewebsites.net/api/claves-api/consumo \
  -H "X-Api-Key: $FACTUZA_CLAVE"

{ "desde": "2026-08-01…", "hasta": "2026-09-01…",
  "porEmisor": [ { "nif": "B87654323", "registros": 12,
                   "incluidos": 3000, "restantes": 2988,
                   "exceso": 0, "eurosDeExceso": 0 } ],
  "totalRegistros": 12, "peticionesPorMinuto": 120 }

4. Los cuatro flujos que importan

Emitir

POST /facturas para el caso simple, POST /facturas/detallada cuando hay varias líneas, descuentos, o tratamientos de IVA distintos del normal (exenta, inversión del sujeto pasivo, no sujeta por localización, suplido). Sin destinatario sale una simplificada (F2), que no da derecho a deducir el IVA a quien la recibe.

Anular y rectificar — no son lo mismo

Una factura ya cobrada no se anula. Es una regla del motor, no de la interfaz.

Gastos

POST /gastos da de alta un gasto deducible, que queda pendiente hasta que alguien lo da por bueno con POST /gastos/{id}/validar. Esa parada es deliberada: un gasto mal leído que entra solo en la contabilidad no se nota hasta que llega el trimestre.

Si tienes la foto o el PDF y no los datos, POST /ocr/analizar devuelve un borrador con lo que ha leído y una lista de en qué no se fía de sí mismo. Nunca crea el gasto: lo propone.

Exportación legal

GET /exportar/registros?desde=&hasta= devuelve los registros de facturación en el formato estandarizado del artículo 10 del RD 1007/2023: un XML por registro, la cadena de huellas en CSV y un manifiesto para poder recalcularlas y comprobar que nadie ha tocado nada. Es lo que hay que poder entregar en una inspección.

GET /exportar/gestoria es otra cosa distinta: el paquete cómodo para el asesor, con PDF y CSV.

Enterarte de lo que dice la AEAT (webhook)

Cuando emites, el 201 llega en milisegundos, pero lo que te está diciendo es «recogido y encolado». La AEAT contesta después, y esa respuesta —aceptado, aceptado con errores, o rechazado con un código— es la que decide si la factura vale. Puedes consultarla con GET /facturas/{id}, pero sondear una por una es caro y en la práctica no lo hace nadie: los rechazos se descubren semanas más tarde, cuando ya has emitido cien más con el mismo error.

Así que dinos dónde avisarte. Desde Área → Claves de API → Webhook, o por la propia API:

PUT /webhook
{ "url": "https://api.tu-erp.es/factuza/avisos" }

→ 200 { "secreto": "whsec_…", "cabecera": "X-Factuza-Firma" }

El secreto se enseña una sola vez. Guárdalo: es con lo que compruebas que el aviso viene de nosotros. Si lo pierdes, vuelves a guardar la URL y se genera otro (cambiar la URL siempre renueva el secreto, para que el que firmaba hacia tu servidor anterior deje de valer).

Cada aviso llega como un POST con este cuerpo:

{ "evento": "registro.remitido",
  "registroId": 48211,
  "numSerieFactura": "FA2026-0117",
  "nifEmisor": "B12345674",
  "resultado": "RECHAZADO",
  "codigoError": "1103",
  "descripcionError": "El NIF del destinatario no está identificado",
  "csv": null,
  "ocurridoUtc": "2026-08-25T09:14:03Z" }

resultado es ACEPTADO, ACEPTADO_ERRORES o RECHAZADO. Solo avisamos de estados definitivos: si la AEAT no está disponible y hay que reintentar, eso es cosa nuestra y no te lo contamos.

Cómo se comprueba la firma

La cabecera X-Factuza-Firma viene así: t=1756112043,v1=<hex>. El v1 es el HMAC-SHA256 de la cadena «<t>.<cuerpo tal cual llegó>» con tu secreto.

firmado = t + "." + cuerpoCrudo
esperado = hmac_sha256(secreto, firmado).hex()
valido   = comparacionEnTiempoConstante(esperado, v1) and (ahora - t) < 300

Compara el tiempo también. La marca t va dentro de lo firmado precisamente para eso: sin comprobarla, cualquiera que intercepte un aviso puede reenviártelo meses después y la firma seguirá cuadrando. Rechaza lo que llegue con más de cinco minutos.

Y usa el cuerpo crudo, antes de parsear el JSON: si lo reserializas, el orden de las claves o los espacios pueden cambiar y la firma dejará de coincidir.

Reintentos y duplicados

Si tu servidor no contesta un 2xx en diez segundos, reintentamos a 1, 5, 15, 60, 180, 360 y 720 minutos, y nos rendimos al octavo intento —unas veinte horas—. En el área verás cada entrega y por qué falló.

Cada reintento lleva la misma cabecera X-Factuza-Entrega, así que si recibiste el aviso pero te caíste antes de contestar, lo reconocerás por ese número y no lo procesarás dos veces. Contesta 200 primero y haz el trabajo después: lo que tardes dentro de la petición cuenta contra esos diez segundos.

Dos cosas que rechazamos y conviene saber de antemano: la URL tiene que ser https:// y apuntar a un servidor público —un aviso lleva el NIF del obligado dentro, y comprobamos la dirección justo antes de llamar—, y no seguimos redirecciones: si contestas un 302 lo contamos como fallo. Apunta el webhook a la URL final.

5. Cuotas y límites

LímiteCuántoQué pasa al pasarse
Registros por emisor y mes 3.000 incluidos Nada se bloquea. El exceso se factura a 2 € por cada 1.000.
Peticiones por minuto 120 429 con cabecera Retry-After.

La asimetría es deliberada y vale la pena entenderla: un tope mensual que cortara la emisión dejaría a un obligado tributario sin poder cumplir la ley por un asunto comercial nuestro. Emitir una factura no es un capricho, tiene fecha. Así que el tope mensual avisa y se cobra.

El límite por minuto sí corta, y por un motivo distinto: un bucle roto en una integración no es la obligación legal de nadie — es una avería que, sin freno, se lleva por delante el servicio de los demás. El 429 además te avisa de que tienes un fallo.

6. Errores

CódigoQué significa
400El cuerpo no vale. El mensaje dice qué campo y por qué, en castellano.
401Sin credencial válida: falta la cabecera, la clave no existe o está revocada.
403Tu cuenta no puede hacer eso: rol insuficiente, licencia no vigente, o un emisor que no es tuyo.
404No existe — o no es tuyo. Pedir el recurso de otra cuenta responde igual que pedir uno que no existe, a propósito.
409Conflicto: ya existe, o el estado no permite la operación (anular una factura cobrada).
429Demasiadas peticiones por minuto. Espera lo que diga Retry-After.

Los errores traen un cuerpo JSON con error y, cuando ayuda, un detalle. Están escritos para que se entiendan sin conocer nuestras tripas: «No puedes emitir facturas a nombre de X. Tu cuenta emite como Y» dice qué pasa y cómo se arregla.

Lo más usado. Todas cuelgan de https://func-factuza-prod-obt1.azurewebsites.net/api en producción y de …-test-… en pruebas.

VerboRutaQué hace
get/emisoresCon qué emisores trabaja tu clave
get/seriesTus series y el siguiente número
post/facturasEmitir (base y tipo únicos)
post/facturas/detalladaEmitir con líneas y tratamientos de IVA
get/facturasListado, con filtros
get/facturas/{id}/pdfEl PDF con su QR
post/facturas/{id}/anularAnular (no borra)
post/facturas/{id}/rectificarRectificar (crea otra)
post/facturas/{id}/emailMandarla por correo
get/gastosGastos del periodo
post/gastosAlta de gasto (queda pendiente)
post/gastos/{id}/validarDarlo por bueno
post/ocr/analizarLeer una foto o un PDF (propone, no crea)
get/maestras/destinatariosTus clientes
get/resumenFacturado, trimestre y pendiente de cobro
get/modelos/{modelo}Borrador del 303, 130, 390…
get/exportar/registrosExportación legal del art. 10
get/claves-api/consumoTu consumo del mes
get/webhookDónde te avisamos y cómo fueron las últimas entregas
put/webhookPoner o cambiar la URL (devuelve el secreto una vez)
delete/webhookDejar de avisar

Hay más de sesenta rutas en total —presupuestos, recurrentes, cobros, series, usuarios—. Si echas en falta alguna, escríbenos y te decimos si existe.

8. Descargar la especificación

El catálogo completo —62 rutas, 82 operaciones— en OpenAPI 3.0, para cargarlo en Postman, Insomnia o el generador de clientes que uses:

Descargar factuza-api.yaml versión 1.0.0 · 25-ago-2026

Se genera del código, no se escribe a mano. Las rutas salen del propio motor y las descripciones del comentario que cada una tiene al lado, y hay una prueba que pone la suite en rojo si publicamos una API con rutas que no están en el fichero. Es la garantía de que lo que te descargas es lo que hay servido.

Lo decimos porque aquí había antes un fichero escrito a mano que describía 19 rutas cuando el motor ya servía 62, con nombres que no existían. Se retiró.

Dos cosas que la especificación todavía no lleva, dichas antes de que las eches en falta: los esquemas de los cuerpos de petición y respuesta —que están en esta página, con ejemplos que se ejecutan en cada pasada de la suite— y la colección de Postman.

9. Compromiso de versión