Anchor es el sistema de facturación que uso como autónomo. Lo he diseñado y programado desde cero para cumplir con VERI*FACTU, el nuevo reglamento de la Agencia Tributaria que obliga a que cada factura que emites quede registrada, encadenada criptográficamente y, en mi caso, enviada a Hacienda en el mismo momento en que la emites.
Es un proyecto personal y privado: no es un SaaS ni está pensado para que lo usen otros. Pero precisamente por eso me ha permitido meterme hasta el fondo en la normativa y tomar todas las decisiones técnicas sin atajos. En este post cuento qué pide la ley, cómo lo he traducido a código y por qué he hecho las cosas como las he hecho.
Un poco de contexto: ¿qué es VERI*FACTU?
Todo empieza con la Ley 11/2021, de medidas de prevención y lucha contra el fraude fiscal (la «Ley Antifraude»). Entre otras cosas, prohíbe el llamado software de doble uso: programas de facturación que permiten ocultar o modificar ventas después de haberlas registrado. A partir de ahí, la ley exige que cualquier programa que emita facturas garantice la integridad, conservación, accesibilidad, legibilidad, trazabilidad e inalterabilidad de los registros.
Esos seis requisitos se concretan en dos normas:
- El Real Decreto 1007/2023, que aprueba el Reglamento de requisitos de los sistemas informáticos de facturación (lo que la AEAT llama SIF).
- La Orden HAC/1177/2024, que baja al detalle técnico: qué campos lleva cada registro, cómo se calcula la huella, cómo es el QR, cómo es el servicio web…
Las fechas se han retrasado ya dos veces. Tras el Real Decreto-ley 15/2025, la obligación arranca el 1 de enero de 2027 para quienes tributan por el Impuesto sobre Sociedades y el 1 de julio de 2027 para el resto, autónomos incluidos. Los servicios de la AEAT, en cambio, ya están operativos, tanto en el entorno de pruebas como en producción.
Dos formas de cumplir
El reglamento permite que un SIF funcione en una de estas dos modalidades:
- VERI*FACTU: cada registro de facturación se envía a la AEAT automáticamente, en el momento. A cambio, el sistema se ahorra la firma electrónica de cada registro y el registro de eventos, y las facturas llevan la leyenda «Factura verificable en la sede electrónica de la AEAT» (o simplemente «VERI*FACTU»).
- NO VERI*FACTU: los registros se quedan en el sistema, firmados con XAdES y acompañados de un registro de eventos también encadenado, y solo se entregan a Hacienda si los requiere.
Anchor es un sistema solo VERI*FACTU. Para alguien que factura poco y quiere dormir tranquilo, tiene todo el sentido: cada factura queda validada por la AEAT en el acto, y el sistema es más sencillo de auditar.
El fabricante también responde
Un detalle que mucha gente pasa por alto: la ley no solo obliga a quien usa el programa, también a quien lo produce. El productor de un SIF debe firmar una declaración responsable en la que certifica que el sistema cumple el reglamento, y esa declaración tiene que estar accesible para quien lo use. Las sanciones por usar o fabricar software que no cumpla son serias, así que esto no es un trámite decorativo.
En Anchor yo soy productor y usuario a la vez. La declaración responsable forma parte del propio sistema (se genera y se consulta desde la aplicación) y su contenido refleja cómo es Anchor: identificador del sistema, versión, número de instalación, que solo funciona en modalidad VERI*FACTU y que no es multiusuario fiscal (un único obligado tributario).
El corazón del sistema: el registro de facturación
Cada vez que emites una factura, el SIF debe generar un registro de facturación de alta. Si después hay que anularla, se genera un registro de anulación. Estos registros son lo que viaja a la AEAT; la factura en PDF es otra cosa distinta.
Cada registro se identifica por el trío NIF del emisor + número de serie + fecha de expedición, y contiene, entre otros datos, el tipo de factura, el desglose de impuestos, la cuota y el importe total, la fecha y hora (con huso horario) en la que se generó el registro, y los datos del propio sistema informático.
Pero la pieza clave es la huella.
La huella y el encadenamiento
Cada registro lleva un hash SHA-256 calculado sobre algunos de sus campos y sobre la huella del registro anterior. Es exactamente la misma idea que una cadena de bloques, pero sin la parte distribuida: si alguien modifica un registro antiguo, su huella cambia, deja de coincidir con la que guarda el registro siguiente y la manipulación se detecta de inmediato.
La especificación de la AEAT es muy concreta sobre cómo calcularla:
- Se toman unos campos fijos, en un orden fijo. Para un alta:
IDEmisorFactura,NumSerieFactura,FechaExpedicionFactura,TipoFactura,CuotaTotal,ImporteTotal,Huella(la del registro anterior) yFechaHoraHusoGenRegistro. Para una anulación, un subconjunto distinto. - Se concatenan como
nombre=valorseparados por&, quitando espacios al principio y al final de cada valor. - Si un campo no tiene valor (por ejemplo, la huella anterior del primer registro), se pone el nombre y el
=sin nada detrás. - Se codifica en UTF-8, se aplica SHA-256 y el resultado se expresa en hexadecimal y en mayúsculas (64 caracteres).
Simplificado, en Go queda así:
type campo struct{ nombre, valor string }
// huella concatena los campos como nombre=valor&nombre=valor…,
// recortando espacios, y devuelve el SHA-256 en hexadecimal mayúsculas.
func huella(campos ...campo) string {
var b strings.Builder
for i, c := range campos {
if i > 0 {
b.WriteByte('&')
}
b.WriteString(c.nombre)
b.WriteByte('=')
b.WriteString(strings.TrimSpace(c.valor))
}
sum := sha256.Sum256([]byte(b.String()))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
Con los datos del primer ejemplo de la propia AEAT:
huella(
campo{"IDEmisorFactura", "89890001K"},
campo{"NumSerieFactura", "12345678/G33"},
campo{"FechaExpedicionFactura", "01-01-2024"},
campo{"TipoFactura", "F1"},
campo{"CuotaTotal", "12.35"},
campo{"ImporteTotal", "123.45"},
campo{"Huella", ""}, // primer registro: sin huella anterior
campo{"FechaHoraHusoGenRegistro", "2024-01-01T19:20:30+01:00"},
)
// 3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60
…que es justo el valor que da la especificación. La función en sí es trivial; lo difícil es todo lo que la rodea. El valor de cada campo tiene que ser exactamente el que va en el XML (mismo formato de fecha, mismos decimales en los importes), el timestamp que se hashea tiene que ser el mismo que se envía, y sobre todo hay que garantizar que nunca haya dos registros que se encadenen al mismo anterior. De eso va el resto del post.
Arquitectura
El proyecto está dividido en dos repositorios:
- Backend en Go con GoFiber, PostgreSQL 18 y Redis 8. Sigue una arquitectura limpia: handlers HTTP finos, servicios de dominio y repositorios. Todo lo específico de la AEAT vive en el paquete
internal/verifactu. - Frontend en React con TanStack Router, Query, Table y Form, validación con Zod y componentes de shadcn/ui sobre Tailwind v4, apoyado en un Design System propio.
Algunas decisiones que creo que merece la pena explicar:
Structs a mano desde los XSD. La AEAT publica los esquemas XSD del servicio. Existen generadores de código para Go, pero preferí transcribir las estructuras a mano con encoding/xml. En un sistema donde un namespace mal puesto o un elemento fuera de orden significa un rechazo, quiero control total sobre las etiquetas, el orden y los prefijos, y quiero poder leer ese código.
Cliente SOAP propio. El ecosistema SOAP en Go es escaso y poco maduro, así que el cliente es un http.Client con TLS mutuo (mi certificado electrónico, cargado desde un PKCS#12 con go-pkcs12) que construye el sobre SOAP y lo envía. Pocas líneas y cero magia.
Guardar los bytes exactos. El XML que se envía y el que se recibe se guardan tal cual en columnas BYTEA. No reconstruyo nada a partir de structs: si algún día hay que demostrar qué se mandó, está el original byte a byte. El resto de datos de la factura que no participan en la huella se guardan en JSONB para poder evolucionar el modelo sin migraciones dolorosas.
IDs públicos y numeración. Las claves primarias nunca salen de la base de datos. Hacia fuera, los clientes tienen un identificador corto en Crockford Base32 (sin caracteres ambiguos como O/0 o I/1), y la numeración de facturas es secuencial por cliente: {id_cliente}-{0001}.
Emitir una factura: todo o nada
En Anchor una factura empieza siendo un borrador, editable sin restricciones. Cuando se emite, deja de poder modificarse y se convierte en algo con consecuencias legales. Ese paso ocurre dentro de una única transacción de PostgreSQL:
El punto delicado es el primero. Para calcular la huella de un registro nuevo necesito la huella del último, y si dos emisiones se ejecutan a la vez, ambas leerían la misma y la cadena se bifurcaría. La solución es una fila chain_state que guarda el último eslabón y que se bloquea con SELECT … FOR UPDATE al inicio de la transacción: cualquier otra emisión espera su turno.
El número de factura se asigna en esa misma transacción. Si cualquier paso falla, el ROLLBACK deshace todo: no se consume número, no se genera huella y no se encola nada. No quedan huecos en la numeración ni eslabones sueltos. Las anulaciones siguen exactamente el mismo camino, generando su propio registro encadenado.
Fíjate en que emitir no envía nada. El registro se deja en una tabla outbox y es otro proceso el que se encarga de hacérselo llegar a la AEAT. Así, emitir una factura no depende de que Hacienda responda en ese momento.
El envío a la AEAT
Del outbox se encarga un dispatcher: un bucle que reclama los registros pendientes con FOR UPDATE SKIP LOCKED. Ese patrón permite que, aunque hubiera varias instancias, cada registro lo coja solo una, y si el proceso se cae a mitad, los bloqueos se liberan y el trabajo vuelve a estar disponible.
Control de flujo
La orden obliga a implementar un control de flujo que no se puede saltar: tras cada envío, la AEAT responde con un TiempoEsperaEnvio (60 segundos de inicio) y el siguiente envío solo puede hacerse cuando haya pasado ese tiempo o cuando haya acumulados 1.000 registros, el máximo por envío, lo que ocurra primero. El dispatcher guarda en Redis cuándo puede volver a enviar y respeta el valor que devuelva cada respuesta.
Leer bien la respuesta
La respuesta tiene un estado global (Correcto, ParcialmenteCorrecto o Incorrecto) y una línea por registro con su propio estado:
- Correcto: registrado sin más.
- AceptadoConErrores: registrado, pero con errores que hay que subsanar.
- Incorrecto: rechazado, con un código de error.
Para casar cada línea con su registro uso el campo RefExterna, que permite enviar un texto libre y que la AEAT devuelve tal cual: ahí va el ULID interno del registro. Además, la respuesta trae un CSV (código seguro de verificación) de la remisión, que según la documentación hay que guardar en ese momento porque no se puede recuperar con consultas posteriores.
Una prioridad que me marqué desde el principio: cualquier error tiene que llegar a la interfaz. Errores de validación de la AEAT, fallos de TLS, timeouts, SOAP Faults… El backend los clasifica y los expone de forma que el frontend pueda mostrar exactamente qué ha pasado con cada registro. En un sistema así, un fallo silencioso es lo peor que puede pasar.
La factura: QR, PDF y correo
El QR tributario
Las facturas de un sistema VERI*FACTU llevan un código QR que cualquiera puede escanear para comprobar en la sede de la AEAT que la factura está registrada. La especificación vuelve a ser muy precisa:
- Entre 30×30 y 40×40 mm, según la ISO/IEC 18004, con nivel de corrección de errores M.
- Precedido del texto «QR tributario:» y seguido de la leyenda VERI*FACTU.
- Al principio de la factura, en la primera página y con margen en blanco alrededor.
- Codifica una URL de cotejo con cuatro parámetros:
nif,numserie,fechaeimporte, correctamente URL-encoded. Ojo con esto: un&dentro del número de serie, sin codificar, partiría la URL en dos parámetros.
Plantillas y PDF
Los PDF se generan con html/template + chromedp: el backend renderiza HTML y una instancia de Chrome headless lo imprime a PDF. Las plantillas se editan desde la propia web con un editor de código (HTML, CSS y la sintaxis de plantillas de Go), con vista previa en vivo contra una factura real o un borrador. Puedo tener varias plantillas (por ejemplo, una en inglés para clientes de fuera), asignar una por defecto a cada cliente y cambiarla en una factura concreta.
Envío por correo
Las facturas se envían por correo con Resend, también con plantillas editables y asuntos personalizables por cliente. Y aquí hay una regla que me parece importante: una factura solo se envía al cliente cuando la AEAT ha aceptado su registro. No tiene sentido mandar una factura con un QR que, al escanearlo, todavía no muestra nada.
La interfaz
El frontend tiene un Design System propio con una estética inspirada en instrumental náutico (de ahí el nombre): azul marino profundo, acentos en latón, tipografía IBM Plex y tokens semánticos para los estados de las facturas (cobrada, pendiente, vencida), con modo claro y oscuro.
Además de lo habitual (panel, facturas, clientes, productos y configuración), incluye un monitor VERI*FACTU con el historial de envíos, el estado de cada registro y el detalle de cada respuesta, y una vista de logs para auditar lo que ha pasado en el sistema.
Estado actual
Ahora mismo Anchor está en la versión 0.4.0 y funcionando contra el entorno de pruebas de la AEAT:
- ✅ Clientes (españoles, UE y extracomunitarios), productos y tratamientos fiscales, incluida la retención de IRPF.
- ✅ Borradores y emisión con numeración, huella y encadenamiento.
- ✅ Envío a la AEAT con control de flujo y gestión completa de respuestas.
- ✅ Anulación de facturas.
- ✅ QR, plantillas de PDF y envío por correo.
- 🔜 Facturas rectificativas.
- 🔒 La emisión en producción está bloqueada a propósito hasta que el sistema esté terminado. Una factura emitida en producción es real y no tiene vuelta atrás.
Lo que me llevo
Lo más interesante de este proyecto no ha sido escribir Go, sino leer normativa y convertirla en invariantes de código. Cada frase de la orden acaba siendo una restricción: un bloqueo en la base de datos, un orden de elementos en un XML, un campo que no puede cambiar nunca o un error que no puede quedar oculto.
También me ha enseñado a pensar en un sistema donde los errores no se pueden deshacer. En la mayoría de aplicaciones un bug se arregla con un despliegue; aquí, un registro mal enviado queda en Hacienda y hay que corregirlo siguiendo el procedimiento. Eso cambia mucho la forma de diseñar: transacciones atómicas, outbox, guardar evidencias de todo y probar en preproducción antes de tocar nada real.
