Integración de VeriFactu con Node.js
Si estás desarrollando un ERP, un SaaS de facturación o cualquier software que necesite cumplir con la normativa VeriFactu, esta guía te muestra el camino más corto desde cero hasta emitir tu primera factura verificada con Node.js.
VeriFactuAPI es un servicio SaaS que abstrae toda la complejidad del protocolo de la AEAT: la generación del XML firmado, la cadena de huellas, el envío y la gestión de respuestas. Tú llamas a una API REST y nosotros nos encargamos del resto. No necesitas certificados propios, ni lidiar con los XSD de Hacienda, ni mantener la infraestructura de envío.
Esta guía usa fetch nativo y JSON, ambos disponibles en Node.js desde la versión 18 sin ninguna instalación adicional. La API es REST pura y puedes integrarla con las APIs estándar de JavaScript.
Requisitos e instalación
No necesitas instalar librerías adicionales para esta integración. Solo necesitas:
- Node.js 18 o superior (incluye
fetchnativo) - Una cuenta en verifactuapi.es (el plan gratuito incluye entorno de pruebas)
- APIs nativas:
fetch,JSON.stringify/JSON.parsey la cabeceraAuthorization: Bearer <token>
No existe un SDK oficial específico para Node.js: la integración se hace directamente contra la API REST, lo cual significa cero dependencias propietarias y total control sobre tu código.
Flujo completo: de la autenticación a la factura registrada
El flujo para enviar una factura tiene tres pasos: autenticarte, generar el registro de facturación y comprobar su estado en la AEAT. Esta comprobación puede hacerse mediante webhook (recomendado) o con consulta manual (polling).
Paso 1 — Autenticación y obtención del token
const VERIFACTU_BASE_URL = 'https://app.verifactuapi.es/api';
async function login(email, password) {
let response;
try {
response = await fetch(`${VERIFACTU_BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ email, password }),
signal: AbortSignal.timeout(30_000),
});
} catch (err) {
throw new Error(`Error de red en login: ${err.message}`);
}
let data;
try {
data = await response.json();
} catch {
throw new Error('Respuesta JSON inválida en login.');
}
if (!data?.success) {
throw new Error(`Login fallido: ${data?.code}, ${data?.message}`);
}
return data.token;
}Ejemplo de uso
const token = await login('tu@email.com', 'tu_password');Paso 2 — Generar registro de facturación
Este es el núcleo de la integración. El siguiente ejemplo cubre el caso más habitual: una factura ordinaria (F1) con IVA al 21%.
async function registrarFactura(token, factura) {
let response;
try {
response = await fetch(`${VERIFACTU_BASE_URL}/alta-registro-facturacion`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(factura),
signal: AbortSignal.timeout(30_000),
});
} catch (err) {
throw new Error(`Error de red al registrar factura: ${err.message}`);
}
let data;
try {
data = await response.json();
} catch {
throw new Error('Respuesta JSON inválida al registrar factura.');
}
if (!data?.success) {
throw new Error(`Error al registrar factura: ${data?.message}`);
}
return data.data.items[0];
}Ejemplo de uso
const datosFactura = {
IDEmisorFactura: 'A39200019',
NumSerieFactura: 'FAC/2025/0001',
FechaExpedicionFactura: '2025-04-22',
TipoFactura: 'F1',
DescripcionOperacion: 'Servicios de desarrollo web - Abril 2025',
EmitidaPorTercODesti: null,
Destinatarios: [
{
NombreRazon: 'Cliente Ejemplo S.A.',
NIF: '39707287H',
},
],
Desglose: [
{
Impuesto: 1,
ClaveRegimen: 1,
CalificacionOperacion: 1,
TipoImpositivo: 21,
BaseImponibleOImporteNoSujeto: 1000,
CuotaRepercutida: 210,
},
],
CuotaTotal: 210,
ImporteTotal: 1210,
};
const factura = await registrarFactura(token, datosFactura);
const registroId = factura.id;
const qrImageB64 = factura.qr_image;
const urlQr = factura.url_qr ?? null;
const estadoAeat = factura.estado_aeat;
console.log(`Factura registrada con ID: ${registroId}`);
console.log(`Estado inicial: ${estadoAeat}`);
console.log(`URL validación AEAT: ${urlQr ?? 'No disponible'}`);Paso 3 — Consultar el estado en la AEAT
El envío a la AEAT es asíncrono. La factura se registra inmediatamente en nuestra plataforma con estado "No Registrado" y se envía en el siguiente ciclo de proceso. Puedes consultar el estado de dos formas:
Opción A — Polling manual
async function consultarEstado(token, registroId) {
let response;
try {
response = await fetch(
`${VERIFACTU_BASE_URL}/alta-registro-facturacion/${registroId}`,
{
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
signal: AbortSignal.timeout(30_000),
}
);
} catch (err) {
throw new Error(`Error de red al consultar estado: ${err.message}`);
}
let data;
try {
data = await response.json();
} catch {
throw new Error('Respuesta JSON inválida al consultar estado.');
}
if (!data?.success) {
throw new Error(`Error al consultar el estado de la factura: ${data?.message}`);
}
const items = data.data.items;
return {
estado_aeat: items.estado_aeat,
codigo_error_aeat: items.codigo_error_aeat,
descripcion_error_aeat: items.descripcion_error_aeat,
};
}Ejemplo de uso
const estado = await consultarEstado(token, registroId);
console.log(`Estado AEAT: ${estado.estado_aeat}`);
// Valores posibles: "No Registrado" | "Pendiente" | "Incorrecto" | "Correcto"Opción B — Webhook (recomendado)
Configura un endpoint en tu aplicación y registra la URL en el panel de VeriFactuAPI. Recibirás una notificación en cuanto la AEAT responda, sin necesidad de hacer polling:
{
"estado": "Correcto",
"num_serie": "FAC/2025/0001",
"factura_id": 1,
"codigo_error": "",
"descripcion_error": "",
"fecha_notificacion": "YYYY-MM-DD HH:MM:SS"
}Formatos de respuesta JSON
Las respuestas de la API siguen una estructura JSON consistente. Entender estos formatos te permite validar respuestas de forma uniforme en tus funciones fetch.
Formato 1 — success (operación correcta)
{
"success": true,
"message": "Inicio de sesión exitoso",
"code": 200,
"token": "eyJ...",
"expires_at": "2026-04-24 18:30:00"
}success:truecuando la operación finaliza correctamente.message: texto descriptivo del resultado.code: código HTTP devuelto por la API.
Campos opcionales: token, expires_at, user_name, user_email, api_key, etc.
Formato 2 — fail (error)
{
"success": false,
"message": "El campo IDEmisorFactura es obligatorio.",
"error": "VALIDATION_ERROR",
"code": 400
}success:falsecuando la operación falla.message: descripción del error para mostrar o registrar en logs.error: código interno o técnico del error.code: código HTTP asociado al fallo.
Formato 3 — list (listados)
{
"success": true,
"message": "Invoice listed successfully",
"code": 200,
"data": {
"items": [],
"count": 0
},
"pagination": {
"total": 0,
"perPage": 0,
"currentPage": 0,
"lastPage": 0
}
}data.items: array con resultados.data.count: número de elementos devueltos en la página actual.pagination: bloque con metadatos de paginación.
Próximos pasos
- Consulta la referencia completa de la API para ver todos los parámetros disponibles (facturas rectificativas, TicketBAI, webhooks, etc.)
- Revisa las listas de valores (
/api/listas) para conocer todos los códigos válidos deTipoFactura,Impuesto,ClaveRegimenyCalificacionOperacion
¿Listo para probarlo?
Crea tu cuenta gratuita y emite tu primera factura verificada en menos de 10 minutos. El entorno de pruebas está completamente habilitado desde el primer día: sin tarjeta, sin límites artificiales, con la misma API que usarás en producción.