Cómo usar la API de Verifactu: enviar, subsanar, anular y obtener el QR
El plugin Verifactu añade a la API de FacturaScripts el endpoint verifactu. Con él puedes enviar facturas de venta a la AEAT, subsanarlas, anularlas y obtener su código QR desde otra aplicación, como tu tienda online, un TPV o un ERP. En esta guía tienes los requisitos, los parámetros, ejemplos con curl y PHP, los errores posibles y cómo saber si Hacienda ha aceptado cada factura.
Antes de empezar
- Plugin configurado: la empresa debe tener el certificado cargado y el ejercicio en modo Verifactu o NO Verifactu. Lo explicamos en cómo configurar Verifactu.
- Cron activo: la API no envía nada a la AEAT por sí misma. Prepara el registro de facturación y el cron de Verifactu lo firma y lo envía. Debe ejecutarse cada 60 segundos.
- API activada y una clave: sigue la guía de la API REST de FacturaScripts. Todas las peticiones llevan la cabecera
Tokencon tu clave. - Permisos de la clave: si la clave no tiene acceso completo, dale acceso al recurso
verifactu: POST para enviar, subsanar y anular, y GET para el QR. Para consultar el resultado necesitas además GET enfacturaclientesyverifacturegistrofacturas. - Haz las pruebas en modo de pruebas: mientras la empresa tenga activado el modo de pruebas, los registros van al entorno de pruebas de la AEAT y no a Hacienda.
El endpoint verifactu
La URL es https://tu-dominio/api/3/verifactu. Siempre recibe dos parámetros, que van en la URL (query string), también en las peticiones POST:
idfactura: el identificador interno de la factura de venta, no su código.action: la operación que quieres hacer.
| action | Método | Qué hace | Cuándo se puede usar |
|---|---|---|---|
alta | POST | Crea el registro de alta de la factura y lo deja en cola para enviarlo | La factura no se ha enviado, no está anulada y no tiene un alta pendiente |
subsanacion | POST | Vuelve a enviar los datos del cliente de una factura ya enviada | La AEAT ya ha aceptado el alta |
anulacion | POST | Anula la factura en la AEAT | La AEAT ya ha aceptado el alta |
qr | GET | Devuelve la imagen del QR tributario | La factura tiene registros y no está anulada |
Enviar una factura (alta)
Hay dos formas de hacerlo.
Opción 1: al cambiar el estado de la factura
Marca la opción Enviar a Verifactu en un estado de factura de cliente, como se explica en cómo enviar una factura a Verifactu. Después:
- Crea la factura con el endpoint crearFacturaCliente. La respuesta incluye el
idfactura. - Actualiza la factura con un PUT a
/api/3/facturaclientes/{idfactura}y pon enidestadoel estado que hace el envío.
Es la opción más cómoda si ya trabajas con estados: la factura se envía igual desde la API que desde el programa.
Opción 2: con la acción alta
Indica tú qué factura quieres enviar:
curl -X POST "https://tu-dominio/api/3/verifactu?idfactura=123&action=alta" \
-H "Token: TU_CLAVE"
Respuesta:
{"status":"success","message":"Invoice sent to Verifactu successfully"}
Importante: esta respuesta significa que el registro se ha creado y está en la cola, no que la AEAT lo haya aceptado. El cron lo enviará en el siguiente minuto. Para conocer el resultado, consulta el estado del registro como se explica a continuación.
Comprobar si la AEAT ha aceptado la factura
Forma rápida
Consulta la factura con un GET a /api/3/facturaclientes/123. El campo vf_sent pasa a true cuando la AEAT acepta el alta, aunque sea con errores.
Con el detalle de cada registro
Cada alta, subsanación o anulación crea un registro de facturación. Puedes listarlos con un GET al recurso verifacturegistrofacturas, filtrando por factura:
curl "https://tu-dominio/api/3/verifacturegistrofacturas?filter[idfactura]=123&sort[id]=DESC" \
-H "Token: TU_CLAVE"
Los campos más útiles son:
event:Alta,SubsanacionoAnulacion.mode:verifactuono-verifactu.status: el resultado.csvytimestamp_presentacion: el código seguro de verificación y la fecha y hora en que la AEAT recibió el registro.response: la respuesta de la AEAT. Aquí verás el motivo si la ha rechazado.
| status | Significado |
|---|---|
| (vacío) | Pendiente: el cron todavía no lo ha procesado |
Correcto | Aceptado por la AEAT |
AceptadoConErrores | Aceptado, pero la AEAT avisa de algún dato a revisar |
Incorrecto | Rechazado por la AEAT. Revisa response |
ErrorHash o ErrorSignature | Error al calcular la huella o la firma antes del envío. Revisa los logs |
En modo NO Verifactu los registros no se envían a la AEAT: se firman y se guardan en tu instalación por si te los pide en un requerimiento.
Si consultas el estado de forma periódica, hazlo cada minuto o más: el cron no procesa los registros con más frecuencia. Tienes más formas de filtrar en listar, filtrar y ordenar registros desde la API.
Subsanar una factura
La subsanación solo corrige los datos del cliente de una factura ya aceptada: CIF/NIF, nombre o razón social, dirección, apartado, código postal, ciudad, provincia y país. Los importes, las líneas y la fecha no se pueden cambiar.
- Actualiza los datos del cliente en la factura con un PUT a
/api/3/facturaclientes/{idfactura}. Los campos soncifnif,nombrecliente,direccion,apartado,codpostal,ciudad,provinciaycodpais. - Llama a la acción
subsanacion:
curl -X POST "https://tu-dominio/api/3/verifactu?idfactura=123&action=subsanacion" \
-H "Token: TU_CLAVE"
Puedes subsanar una factura todas las veces que necesites. Más información en cómo subsanar una factura en Verifactu.
Anular una factura
curl -X POST "https://tu-dominio/api/3/verifactu?idfactura=123&action=anulacion" \
-H "Token: TU_CLAVE"
La anulación no tiene vuelta atrás y el QR de la factura deja de valer. Si te equivocaste en los importes o en las líneas, lo normal es hacer una factura rectificativa con el endpoint crearFacturaRectificativaCliente y enviarla como cualquier otra factura. Anula solo las facturas que ya no son válidas. Lo explicamos en cómo anular una factura en Verifactu.
Obtener el QR
curl "https://tu-dominio/api/3/verifactu?idfactura=123&action=qr" \
-H "Token: TU_CLAVE"
La respuesta no es JSON: es directamente la imagen PNG codificada en base64, con este formato:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
Puedes usarla tal cual en el atributo src de una etiqueta <img> o decodificarla para guardar el PNG. El QR está disponible en cuanto existe el registro de alta, aunque el cron todavía no lo haya enviado. Si la factura está anulada, la API devuelve un error.
Respuestas de error
Los errores responden con un JSON como este:
{"status":"error","message":"Invoice not found"}
| Código HTTP | message | Causa |
|---|---|---|
| 400 | Invalid action | El parámetro action falta o no es alta, subsanacion, anulacion ni qr |
| 405 | Method not allowed for QR action | Has pedido el QR con un método distinto de GET |
| 405 | Method not allowed for this action | Has pedido alta, subsanación o anulación con un método distinto de POST |
| 404 | Invoice not found | No existe ninguna factura de venta con ese idfactura |
| 400 | Invoice already sent to Verifactu | La AEAT ya aceptó el alta de esa factura |
| 400 | Invoice not sent to Verifactu | Has intentado subsanar o anular una factura cuya alta no ha aceptado la AEAT |
| 400 | Invoice has no records in Verifactu | Has pedido el QR de una factura sin registros |
| 400 | Invoice is cancelled in Verifactu | Has pedido el QR de una factura anulada |
| 500 | Error sending invoice to Verifactu | No se pudo crear el registro de alta. Por ejemplo, ya hay un alta pendiente de enviar, la factura está anulada o la empresa no está configurada |
| 500 | Error correcting invoice in Verifactu | No se pudo crear la subsanación. Por ejemplo, ya hay otra pendiente |
| 500 | Error cancelling invoice in Verifactu | No se pudo crear la anulación. Por ejemplo, ya hay otra pendiente |
Ante un error 500, mira el motivo en Informes → Verifactu, pestaña Logs, o en Administrador → Logs. Si el error es de la clave o de la versión de la API, consulta la guía de la API REST de FacturaScripts.
Ejemplo completo en PHP
Este ejemplo envía una factura existente, espera a que la AEAT responda y guarda el QR:
<?php
$url = 'https://tu-dominio/api/3/';
$token = 'TU_CLAVE';
$idfactura = 123;
function api(string $method, string $endpoint): array
{
global $url, $token;
$ch = curl_init($url . $endpoint);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ['Token: ' . $token],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
return [$code, $body];
}
// 1. dejamos la factura en la cola de envío
[$code, $body] = api('POST', 'verifactu?idfactura=' . $idfactura . '&action=alta');
if ($code !== 200) {
exit('Error ' . $code . ': ' . $body . PHP_EOL);
}
// 2. esperamos a que el cron la envíe (máximo 10 minutos)
for ($i = 0; $i < 10; $i++) {
sleep(60);
[, $body] = api('GET', 'verifacturegistrofacturas?filter[idfactura]=' . $idfactura
. '&filter[event]=Alta&sort[id]=DESC&limit=1');
$status = json_decode($body, true)[0]['status'] ?? '';
if ($status !== '') {
echo 'Estado: ' . $status . PHP_EOL;
break;
}
}
// 3. guardamos el QR en un PNG
[$code, $qr] = api('GET', 'verifactu?idfactura=' . $idfactura . '&action=qr');
if ($code === 200) {
file_put_contents('qr-' . $idfactura . '.png', base64_decode(explode(',', $qr, 2)[1]));
}