Webhooks
La remisión a la AEAT es asíncrona. Un webhook avisa a tu sistema cuando un registro termina —aceptado, rechazado, subsanado— sin que tengas que sondear la API.
Crear una suscripción
Desde la consola (Webhooks → Nuevo) o con POST https://verifactu-api.zctechpartners.cloud/v1/webhooks y una clave administrativa: una URL HTTPS de tu sistema y la lista de eventos. La respuesta incluye una sola vez el secret de firma; guárdalo en la configuración del servidor que recibe las entregas.
{
"url": "https://tu-sistema.com/webhooks/verifactu",
"eventos": ["registro.aceptado", "registro.aceptado_con_errores", "registro.rechazado", "registro.subsanado", "registro.reenvio_agotado"]
}data.obligado_nif y por el registro consultado.Eventos
| type | Cuándo se emite |
|---|---|
registro.aceptado | La AEAT aceptó el registro (EstadoRegistro Correcto). |
registro.aceptado_con_errores | Aceptado con incidencias no bloqueantes (AceptadoConErrores). Conviene revisar el detalle. |
registro.rechazado | Rechazado por la AEAT (Incorrecto). data.codigo trae el código de error. |
registro.subsanado | Se emitió y aceptó la subsanación de un registro rechazado. |
registro.reenvio_agotado | Se agotaron los reintentos de envío: nada quedó registrado en la AEAT y hace falta intervenir. |
obligado.certificado_por_caducar | El certificado de un obligado caduca pronto. |
obligado.legitimacion_fallida | La AEAT rechazó la legitimación del certificado o del apoderamiento. |
La entrega
Cada evento es un POST a tu URL con un cuerpo JSON estable:
X-Verifactu-Signature: t=1788912345,v1=5f2a…c9
Content-Type: application/json
{
"id": "evt_9c1d…",
"type": "registro.rechazado",
"created_at": "2027-01-07T10:15:03.000Z",
"data": {
"registro_id": "0d3f2c1e-…",
"obligado_nif": "B21778451",
"codigo": 1234
}
}El cuerpo lleva lo justo para saber qué registro y qué pasó; el detalle (descripción del error, CSV, historial) se consulta con GET /v1/registros/{registro_id}. Así un webhook interceptado no expone datos fiscales, y tu sistema siempre lee el estado actual y no una foto antigua.
Responde 2xx en pocos segundos. Cualquier otra respuesta, o un timeout, cuenta como fallo y se reintenta.
Verificar la firma
La cabecera X-Verifactu-Signature tiene la forma t=<epoch en segundos>,v1=<hmac>. El HMAC es SHA-256 con tu secreto sobre la cadena "<t>.<cuerpo crudo>". Verifica siempre antes de procesar:
- Lee el cuerpo crudo (bytes tal cual), antes de parsear el JSON.
- Separa
tyv1de la cabecera. - Calcula
HMAC-SHA256(secreto, t + "." + cuerpo)en hexadecimal. - Compara con
v1en tiempo constante. - Rechaza si
tse aleja más de 5 minutos de tu reloj (protección contra reenvíos).
import { createHmac, timingSafeEqual } from "node:crypto"
export async function POST(req: Request) {
const cuerpo = await req.text() // crudo, sin parsear
const cabecera = req.headers.get("x-verifactu-signature") ?? ""
const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=")))
const t = Number(partes.t)
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) {
return new Response("firma caducada", { status: 401 })
}
const esperado = createHmac("sha256", process.env.VERIFACTU_WEBHOOK_SECRET!)
.update(`${t}.${cuerpo}`).digest("hex")
const a = Buffer.from(esperado), b = Buffer.from(String(partes.v1 ?? ""))
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return new Response("firma inválida", { status: 401 })
}
const evento = JSON.parse(cuerpo)
// … procesar evento.type / evento.data.registro_id …
return new Response(null, { status: 204 })
}Reintentos e idempotencia
Si tu endpoint no responde 2xx, la API reintenta con espera creciente —1 minuto, 4, 16, luego cada hora— hasta 6 intentos; el despachador pasa cada 15 minutos, así que el espaciado real nunca baja de eso. Agotados los intentos, la entrega queda marcada como fallida y se ve en el registro.
Un mismo evento puede llegar más de una vez (por ejemplo, si respondiste 2xx tarde). Usa id (evt_…) para deduplicar, y trata el procesamiento como idempotente: leer el estado actual del registro y guardarlo.
registro.aceptado (o el rechazo, si el cuerpo tenía un error de fondo). Ver también Consultar el estado.