Para desenvolvedores

A API de Factuza

O mesmo motor que usan a web e a app: pegada encadeada SHA-256, numeración por serie, PDF con QR de comprobación e remisión á AEAT.

Estado: preview para integradores. A API funciona e está probada, e os prezos e o reparto de responsabilidades están na páxina do Motor API. A alta aínda non ten botón: faise falando. Escríbenos a hola@factuza.com e dámosche acceso ao contorno de probas o mesmo día.

Isto non é unha lista de intencións: cada exemplo desta páxina execútase en cada pasada da batería de probas (caso CU-K04). Se algo deixase de funcionar, a proba ponse vermella antes de que o descubras ti.

1. Para quen é isto

Para software que xa xestiona un negocio —un taller, unha clínica, unha academia, un ERP vertical— e precisa que as súas facturas cumpran VeriFactu sen reescribir a parte fiscal. O teu sistema segue sendo o que manda; Factuza pon a numeración, a pegada encadeada, o PDF e a remisión.

O que non é: unha pasarela de sinatura nin un simple xerador de PDF. Cada factura que emites por aquí nace co seu rexistro de facturación, entra na cadea de pegadas e remítese á Axencia Tributaria igual que se a emitises desde a web.

Unha factura emitida non se borra. Entra na cadea de pegadas e alí queda. O que existe é anular e rectificar, que son dúas operacións distintas e as dúas deixan rastro. Proba no contorno de probas antes de emitir de verdade.

2. Autenticación: a clave de API

Toda a API acepta dúas credenciais: o testemuño dunha persoa (o que usan a web e a app) ou unha clave de API, que é a dunha máquina. Para integrar, a túa é a segunda.

Unha chave créase desde a área de clientes, en Chaves de API, co teu usuario de administrador, e viaxa nunha cabeceira:

X-Api-Key: fzk_x8Kq2vN...

Ao crreala escolles que pode facer, e a diferenza importa:

Se a túa integración non incorpora clientes, usa a primeira: unha chave que pode menos fai menos dano o día que se filtre. E non se pode ampliar sobre a marcha — créase outra a propósito.

Tres regras que convén saber antes

A clave herda os permisos da túa conta: os seus emisores, o seu plan e a súa licenza. Non pode emitir a nome dun NIF que non sexa o teu, e se a túa licenza caduca deixa de emitir igual que a web — pero segue podendo consultar e descargar o xa emitido, porque os teus libros son teus.

3. A túa primeira factura, paso a paso

Cinco chamadas. Todas coa mesma cabeceira e ningunha con testemuño de usuario. O exemplo usa o contorno de probas, onde nada conta nin se remite á AEAT.

01Quen son?

Antes de nada, con que emisores pode traballar a túa clave. De aquí sae o NIF e o nome que van na factura: non os tecleés, léeos.

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 }

02A túa serie

O número de factura ponno o servidor, dentro da mesma transacción que a emite. Ti mandas o serieId, non o número: teclealo desde fóra deixa a correlatividade en mans de que ninguén se equivoque, e un oco na numeración hai que xustificalo ante Facenda.

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

O corpo mínimo dunha factura normal (F1) cunha soa base e un só tipo. Para varias liñas ou tratamentos especiais de IVE, 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 —creouse un recurso, non é un 200— co número que puxo a serie e a pegada:

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

Sen pegada non é VeriFactu. Son 64 caracteres: o SHA-256 do rexistro, encadeado co da factura anterior.

04O PDF

Co seu QR de comprobación, listo para mandarllo ao cliente.

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

05Como vas de consumo

Esta ruta si a pode chamar unha máquina, ao revés que as de xestión de claves: negarche saber por onde vas e logo cobrarche o exceso sería tenderche unha 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. Os catro fluxos que importan

Emitir

POST /facturas para o caso simple, POST /facturas/detallada cando hai varias liñas, descontos, ou tratamentos de IVE distintos do normal (exenta, inversión do suxeito pasivo, non suxeita por localización, suplido). Sen destinatario sae unha simplificada (F2), que non dá dereito a deducir o IVE a quen a recibe.

Anular e rectificar — non son o mesmo

Unha factura xa cobrada non se anula. É unha regra do motor, non da interface.

Gastos

POST /gastos dá de alta un gasto deducible, que queda pendente ata que alguén o dá por bo con POST /gastos/{id}/validar. Esa parada é deliberada: un gasto mal lido que entra só na contabilidade non se nota ata que chega o trimestre.

Se tes a foto ou o PDF e non os datos, POST /ocr/analizar devolve un borrador co que leu e unha lista de en que non se fía de si mesmo. Nunca crea o gasto: propono.

Exportación legal

GET /exportar/registros?desde=&hasta= devolve os rexistros de facturación no formato estandarizado do artigo 10 do RD 1007/2023: un XML por rexistro, a cadea de pegadas en CSV e un manifesto para poder recalculalas e comprobar que ninguén tocou nada. É o que hai que poder entregar nunha inspección.

GET /exportar/gestoria é outra cousa distinta: o paquete cómodo para o asesor, con PDF e CSV.

Enterarte do que di a AEAT (webhook)

Cando emites, o 201 chega en milisegundos, pero o que che está a dicir é «recollido e encolado». A AEAT contesta despois, e esa resposta —aceptado, aceptado con erros, ou rexeitado cun código— é a que decide se a factura vale. Podes consultala con GET /facturas/{id}, pero sondar unha por unha é caro e na práctica non o fai ninguén: os rexeitamentos descóbrense semanas máis tarde, cando xa emitiches cen máis co mesmo erro.

Así que dinos onde avisarte. Desde Área → Chaves de API → Webhook, ou pola propia API:

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

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

O segredo amósase unha soa vez. Gárdao: é co que comprobas que o aviso vén de nós. Se o perdes, volves gardar a URL e xérase outro (cambiar a URL sempre renova o segredo, para que o que asinaba cara ao teu servidor anterior deixe de valer).

Cada aviso chega como un POST con este corpo:

{ "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 é ACEPTADO, ACEPTADO_ERRORES ou RECHAZADO. Só avisamos de estados definitivos: se a AEAT non está dispoñible e hai que reintentar, iso é cousa nosa e non cho contamos.

Como se comproba a sinatura

A cabeceira X-Factuza-Firma chega así: t=1756112043,v1=<hex>. O v1 é o HMAC-SHA256 da cadea «<t>.<corpo tal cal chegou>» co teu segredo.

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

Compara o tempo tamén. A marca t vai dentro do asinado precisamente para iso: sen comprobala, calquera que intercepte un aviso pode reenviarcho meses despois e a sinatura seguirá cadrando. Rexeita o que chegue con máis de cinco minutos.

E usa o corpo cru, antes de analizar o JSON: se o reserializas, a orde das chaves ou os espazos poden cambiar e a sinatura deixará de coincidir.

Reintentos e duplicados

Se o teu servidor non contesta un 2xx en dez segundos, reintentamos a 1, 5, 15, 60, 180, 360 e 720 minutos, e rendémonos ao oitavo intento —unhas vinte horas—. Na área verás cada entrega e por que fallou.

Cada reintento leva a mesma cabeceira X-Factuza-Entrega, así que se recibiches o aviso pero caíches antes de contestar, recoñecerálo por ese número e non o procesarás dúas veces. Contesta 200 primeiro e fai o traballo despois: o que tardes dentro da petición conta contra eses dez segundos.

Dúas cousas que rexeitamos e convén saber de antemán: a URL ten que ser https:// e apuntar a un servidor público —un aviso leva o NIF do obrigado dentro, e comprobamos o enderezo xusto antes de chamar—, e non seguimos redireccións: se contestas un 302 contámolo como fallo. Apunta o webhook á URL final.

5. Cotas e límites

LímiteCantoQue pasa ao pasarse
Rexistros por emisor e mes 3.000 incluídos Nada se bloquea. O exceso factúrase a 2 € por cada 1.000.
Peticións por minuto 120 429 con cabeceira Retry-After.

A asimetría é deliberada e paga a pena entendela: un tope mensual que cortase a emisión deixaría a un obrigado tributario sen poder cumprir a lei por un asunto comercial noso. Emitir unha factura non é un capricho, ten data. Así que o tope mensual avisa e cóbrase.

O límite por minuto si corta, e por un motivo distinto: un bucle roto nunha integración non é a obriga legal de ninguén — é unha avaría que, sen freo, leva por diante o servizo dos demais. O 429 ademais avísate de que tes un fallo.

6. Erros

CódigoQue significa
400O corpo non vale. A mensaxe di que campo e por que, en castelán.
401Sen credencial válida: falta a cabeceira, a clave non existe ou está revogada.
403A túa conta non pode facer iso: rol insuficiente, licenza non vixente, ou un emisor que non é teu.
404Non existe — ou non é teu. Pedir o recurso doutra conta responde igual que pedir un que non existe, a propósito.
409Conflito: xa existe, ou o estado non permite a operación (anular unha factura cobrada).
429Demasiadas peticións por minuto. Agarda o que diga Retry-After.

Os erros traen un corpo JSON con error e, cando axuda, un detalle. Están escritos para que se entendan sen coñecer as nosas tripas: «No puedes emitir facturas a nombre de X. Tu cuenta emite como Y» di que pasa e como se arranxa.

O máis usado. Todas colgan de https://func-factuza-prod-obt1.azurewebsites.net/api en produción e de …-test-… en probas.

VerboRutaQue fai
get/emisoresCon que emisores traballa a túa clave
get/seriesAs túas series e o número seguinte
post/facturasEmitir (base e tipo únicos)
post/facturas/detalladaEmitir con liñas e tratamentos de IVE
get/facturasListaxe, con filtros
get/facturas/{id}/pdfO PDF co seu QR
post/facturas/{id}/anularAnular (non borra)
post/facturas/{id}/rectificarRectificar (crea outra)
post/facturas/{id}/emailMandala por correo
get/gastosGastos do período
post/gastosAlta de gasto (queda pendente)
post/gastos/{id}/validarDalo por bo
post/ocr/analizarLer unha foto ou un PDF (propón, non crea)
get/maestras/destinatariosOs teus clientes
get/resumenFacturado, trimestre e pendente de cobro
get/modelos/{modelo}Borrador do 303, 130, 390…
get/exportar/registrosExportación legal do art. 10
get/claves-api/consumoO teu consumo do mes
get/webhookOnde te avisamos e como foron as últimas entregas
put/webhookPoñer ou cambiar a URL (devolve o segredo unha vez)
delete/webhookDeixar de avisar

Hai máis de sesenta rutas en total —orzamentos, recorrentes, cobros, series, usuarios—. Se botas en falta algunha, escríbenos e dicímosche se existe.

8. Descargar a especificación

O catálogo completo —62 rutas, 82 operacións— en OpenAPI 3.0, para cargalo en Postman, Insomnia ou o xerador de clientes que uses:

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

Xérase do código, non se escribe a man. As rutas saen do propio motor e as descricións do comentario que cada unha ten ao lado, e hai unha proba que pon a suite en vermello se publicamos unha API con rutas que non están no ficheiro. É a garantía de que o que descargas é o que hai servido.

Dicímolo porque aquí había antes un ficheiro escrito a man que describía 19 rutas cando o motor xa servía 62, con nomes que non existían. Retirouse.

Dúas cousas que a especificación aínda non leva, ditas antes de que as botes en falta: os esquemas dos corpos de petición e resposta —que están nesta páxina, con exemplos que se executan en cada pasada da suite— e a colección de Postman.

9. Compromiso de versión