Sandbox sobre la preproducción de la AEAT · sin tarjeta · Primera factura en 5 minutos
verifactu.co

Webhooks

Webhooks: que la AEAT te llame a ti

La remisión a la AEAT no es síncrona. En vez de preguntar cada pocos segundos si ya hay respuesta, registras una URL y te avisamos: aceptado, rechazado o error, con el CSV cuando lo hay.

¿Cómo sé si la AEAT ha aceptado una factura?

Con un webhook. Registras una URL y verifactu.co la llama en cuanto la AEAT contesta, con el evento registro.aceptado y el CSV, o con registro.rechazado y el código de error.

Cada envío va firmado con HMAC-SHA256 sobre el timestamp y el cuerpo, y se reintenta ocho veces con espera creciente si tu servidor no responde 2xx.

01 Los eventos Qué te avisamos
EventoCuándo se dispara
registro.aceptadoLa AEAT aceptó el registro. Trae el CSV.
registro.aceptado_con_erroresAceptado con avisos. Casi siempre el formato de importes: merece revisión aunque no bloquee.
registro.rechazadoRechazo de contenido. No se reintenta: hay que corregir un dato.
registro.errorAgotados los seis reintentos de un fallo transitorio.
registro.anuladoEl registro de anulación fue aceptado.
emisor.certificado_por_caducarFaltan 30 días para que caduque el certificado de un emisor.
El de certificado por caducar parece menor y no lo es: un certificado caducado no da un error elegante, da un fallo de TLS que según tu cliente HTTP puede parecer cualquier otra cosa. Para cuando lo diagnosticas llevas facturas sin remitir.
02 El envío Cabeceras y cuerpo
Lo que recibes
POST https://tu-app.example/verifactu
X-Verifactu-Firma: t=1756200000,v1=5f3c9a...
X-Verifactu-Evento: registro.aceptado

{
  "id":      "evt_7Hs2kP",
  "tipo":    "registro.aceptado",
  "creado_en": "2026-08-26T10:16:12+01:00",
  "datos": {
    "id":     "reg_9fK2mQ",
    "numero": "F2026/0184",
    "estado": "aceptado",
    "csv":    "A1B2C3D4E5F6"
  }
}
CabeceraQué es
X-Verifactu-Firmat= timestamp y v1= HMAC-SHA256 de t.cuerpo con tu secreto.
X-Verifactu-EventoEl tipo, para poder enrutar sin parsear el JSON.
03 Verificar la firma No es opcional

Tu endpoint de webhook es una URL pública que marca facturas como aceptadas ante Hacienda. Sin verificar la firma, cualquiera que la descubra puede mentirle a tu contabilidad.

PHP
// Verifica la firma ANTES de tocar el cuerpo.
// Sin esto, cualquiera que conozca tu URL puede marcar
// facturas como aceptadas.

$firma  = $_SERVER['HTTP_X_VERIFACTU_FIRMA'];
$cuerpo = file_get_contents('php://input');

preg_match('/t=(\\d+),v1=([a-f0-9]+)/', $firma, $m);
[, $ts, $recibida] = $m;

// El timestamp entra en la firma: sin él, una petición
// capturada se podría reenviar indefinidamente.
if (abs(time() - (int)$ts) > 300) {
    http_response_code(400); exit;
}

$esperada = hash_hmac('sha256', $ts . '.' . $cuerpo, $secreto);

if (!hash_equals($esperada, $recibida)) {
    http_response_code(400); exit;
}

http_response_code(200);  // responde 2xx rápido
04 Reintentos Si tu servidor no contesta
01

Esperamos un 2xx en 10 segundos

Si tardas más, se considera fallido. Contesta rápido y procesa después: no hagas el trabajo pesado dentro de la petición.

02

Ocho reintentos con espera creciente

1 minuto, 5, 15, 45, 2 horas, 6, 12 y 24. Un despliegue de diez minutos no te hace perder ningún evento.

03

Los eventos pueden llegar desordenados o repetidos

Usa el id del evento para descartar duplicados. Un reintento tras un timeout puede duplicar uno que sí procesaste.

04

Y si todo falla, están en la API

GET /v1/registros?estado=… siempre tiene la verdad. El webhook es una comodidad, no la única fuente.

FAQ Preguntas Respuestas directas
¿Puedo tener varias URLs de webhook?

Sí, y se puede filtrar por tipo de evento en cada una. Es lo habitual cuando los rechazos van a un canal de soporte y las aceptaciones a la contabilidad.

¿Qué hago mientras desarrollo en local?

El sandbox acepta cualquier URL pública, así que sirve cualquier túnel HTTP. También puedes hacer polling de GET /v1/registros mientras montas el endpoint: en desarrollo no molesta a nadie.

¿El webhook sustituye a consultar el registro?

No del todo. El webhook te ahorra el polling, pero la fuente de verdad es siempre el registro en la API. Si tu endpoint estuvo caído más de 24 horas, reconcilia consultando.

Última revisión normativa: Especificaciones de huella AEAT v0.1.2 Revisado por el equipo técnico de verifactu.co