#¿Cómo se integra Verifactu en Node.js?
Para integrar Verifactu en Node.js no necesitas certificados digitales ni XML: se hace a través de una API que actúe como sistema informático de facturación (SIF) certificado. Con BeeL. el flujo es: instalar el SDK
@beel_es/sdk, crear una factura conbeel.invoices.create(), emitirla conbeel.invoices.issue()— BeeL. genera el hash encadenado, el QR oficial y la registra en la AEAT — y escuchar el webhookverifactu.status.updatedpara confirmar la aceptación. Todo el proceso, de cero a factura registrada, lleva unos 10 minutos.
Eso es el resumen. Ahora vamos paso a paso, con código TypeScript completo que puedes copiar y pegar, y la respuesta JSON real de cada llamada.
¿Qué es Verifactu?
Verifactu es el sistema de la AEAT (Real Decreto 1007/2023) que obliga a que todo software de facturación en España firme, encadene con hash SHA-256 y registre cada factura en Hacienda. Es obligatorio para sociedades desde el 1 de enero de 2027 y para autónomos desde el 1 de julio de 2027. Si tu aplicación Node.js emite facturas, esto te afecta directamente.
Casi todos los tutoriales de integración Verifactu que existen son para PHP o para conectarte por SOAP directamente contra la AEAT. Esta guía es lo contrario: Node.js + TypeScript, REST y JSON, usando la API de BeeL. como capa de cumplimiento. BeeL. es el SIF; tú solo haces llamadas HTTP.
#Paso 1: Cuenta y API key de sandbox (2 minutos)
- Crea tu cuenta gratuita en app.beel.es
- Ve a Ajustes → API Keys, pulsa Create API Key y selecciona el entorno sandbox (la key solo se muestra una vez)
Las keys de sandbox llevan el prefijo beel_sk_test_. Como explica la documentación de autenticación, ambos entornos comparten la misma base URL pero los datos están aislados:
| Entorno | Prefijo | ¿Envía a la AEAT? |
|---|---|---|
| Sandbox | beel_sk_test_ | No — datos aislados, facturas ilimitadas que no consumen cuota |
| Producción | beel_sk_live_ | Sí — registro Verifactu real, números legalmente vinculantes |
La base URL es siempre la misma: https://app.beel.es/api/v1, con autenticación Authorization: Bearer <tu-api-key>.
Guarda la key en una variable de entorno (nunca en el código):
export BEEL_API_KEY="beel_sk_test_..."#Paso 2: Instalar el SDK (1 minuto)
npm install @beel_es/sdkUna sola dependencia (openapi-fetch), tipado completo, ESM + CommonJS, Node 18+. Inicializa el cliente:
import { BeeL } from '@beel_es/sdk';
const beel = new BeeL({ apiKey: process.env.BEEL_API_KEY! });El cliente es instance-based (sin estado global), así que funciona igual en un servidor Express que en Vercel o AWS Lambda. Si prefieres REST puro sin SDK, todos los pasos de esta guía tienen su equivalente HTTP en la referencia de la API.
#Paso 3: Crear el cliente al que vas a facturar (2 minutos)
Toda factura estándar necesita un destinatario con NIF y dirección. Créalo una vez y reutiliza su id:
import { CustomerBuilder } from '@beel_es/sdk';
const customerData = CustomerBuilder.create()
.name('Acme SL')
.nif('B12345678')
.email('admin@acme.es')
.address('Calle Mayor', '1', '28001', 'Madrid', 'Madrid', 'Spain')
.build();
const customer = await beel.customers.create(customerData);
console.log(customer.id); // UUID del clienteLa respuesta REST equivalente (POST /v1/customers) llega siempre en el mismo envelope { success, data, meta }:
{
"success": true,
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"legal_name": "Acme SL",
"nif": "B12345678",
"email": "admin@acme.es",
"address": {
"street": "Calle Mayor",
"number": "1",
"postal_code": "28001",
"city": "Madrid",
"province": "Madrid",
"country": "Spain"
},
"active": true,
"created_at": "2026-06-10T09:12:00Z"
},
"meta": {
"timestamp": "2026-06-10T09:12:00Z",
"request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}#Paso 4: Crear la factura en borrador (2 minutos)
En BeeL. una factura nace como borrador (DRAFT): editable, sin número fiscal y sin registro en la AEAT todavía. Referencia completa: createInvoice.
const invoice = await beel.invoices.create({
type: 'STANDARD',
recipient: { customer_id: customer.id },
lines: [
{
description: 'Suscripción mensual — plan Pro',
quantity: 1,
unit_price: 29.00,
discount_percentage: 0,
},
],
});
console.log(invoice.id); // UUID asignado ya
console.log(invoice.status); // "DRAFT"
console.log(invoice.invoice_number); // null — se asigna al emitirRespuesta JSON (campos principales):
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"invoice_number": null,
"series": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "code": "FAC" },
"number": null,
"type": "STANDARD",
"status": "DRAFT",
"issue_date": "2026-06-10",
"totals": {
"taxable_base": 29.00,
"vat_breakdown": [{ "type": 21, "base": 29.00, "amount": 6.09 }],
"total_vat": 6.09,
"total_equivalence_surcharge": 0,
"total_irpf": 0,
"invoice_total": 35.09
},
"created_at": "2026-06-10T09:13:00Z",
"updated_at": "2026-06-10T09:13:00Z"
},
"meta": {
"timestamp": "2026-06-10T09:13:00Z",
"request_id": "9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"
}
}Dos detalles importantes del modelo:
invoice_numberesnullen borradores. El número correlativo se asigna en la emisión — exactamente lo que exige la normativa antifraude.- El endpoint soporta el header
Idempotency-Key: si tu petición se reintenta por un timeout de red, no se crea una factura duplicada. El SDK genera estas claves automáticamente en cada POST.
#Paso 5: Emitir la factura — aquí ocurre Verifactu (1 minuto)
Emitir (issueInvoice, POST /v1/invoices/{id}/issue) pasa la factura de DRAFT a ISSUED: se asigna el número definitivo, se congela el documento y BeeL. genera el registro Verifactu (hash encadenado + QR) y lo envía a la AEAT. Tu código no cambia nada para que esto pase: es el comportamiento por defecto.
const issued = await beel.invoices.issue(invoice.id);
console.log(issued.invoice_number); // "FAC-2026/0042"
console.log(issued.status); // "ISSUED"Respuesta JSON con el bloque verifactu incluido:
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"invoice_number": "FAC-2026/0042",
"status": "ISSUED",
"issue_date": "2026-06-10",
"verifactu": {
"enabled": true,
"submission_status": "PENDING",
"qr_url": "https://verifactu.agenciatributaria.gob.es/v?id=ABC123XYZ"
},
"pdf_download_url": "/v1/invoices/550e8400-e29b-41d4-a716-446655440000/pdf"
},
"meta": {
"timestamp": "2026-06-10T09:14:00Z",
"request_id": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}
}El submission_status arranca en PENDING: BeeL. ya ha enviado el registro a la AEAT y está esperando el veredicto. Cuando la AEAT acepta, el objeto verifactu se completa con invoice_hash (SHA-256 del registro), chaining_hash (encadenamiento con la factura anterior), registration_number y qr_base64 (el QR como PNG en base64, por si renderizas tus propios PDFs). En sandbox nada viaja a la AEAT real, pero el flujo de estados es el mismo.
No hagas polling
Podrías consultar GET /v1/invoices/{id} en bucle hasta ver ACCEPTED, pero hay una forma mejor: el webhook del paso 7.
#Paso 6: Obtener el QR y el PDF (1 minuto)
El PDF se genera con el QR Verifactu, el hash y la leyenda oficial. Con el SDK lo descargas directamente como Buffer:
import fs from 'fs';
const { buffer, fileName } = await beel.downloadPdf(issued.id);
fs.writeFileSync(fileName, buffer); // => factura_FAC-2026-0042.pdfY si prefieres una URL de descarga prefirmada (por ejemplo, para abrirla desde el navegador), el endpoint REST GET /v1/invoices/{id}/pdf (generateInvoicePdf) devuelve una URL temporal que expira en 5 minutos:
{
"success": true,
"data": {
"download_url": "https://minio.beel.es/beel-invoices/invoices/.../factura.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"expires_in_seconds": 300,
"file_name": "factura_FAC-2026-0042.pdf"
},
"meta": {
"timestamp": "2026-06-10T09:15:00Z",
"request_id": "7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b"
}
}Si emites con POST /v1/invoices/{id}/issue?wait_for_pdf=true, la respuesta de emisión ya espera a que el PDF esté generado (~1-2 s extra de latencia). Y si lo que quieres es el QR para incrustarlo en tu propia plantilla, usa verifactu.qr_base64 cuando el registro esté en ACCEPTED.
#Paso 7: Webhook de confirmación de la AEAT (3 minutos)
La AEAT responde de forma asíncrona. Para reaccionar al veredicto en tiempo real, suscríbete al evento verifactu.status.updated:
curl -X POST "https://app.beel.es/api/v1/webhooks" \
-H "Authorization: Bearer $BEEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tuapp.com/webhooks/beel",
"events": ["verifactu.status.updated"]
}'La respuesta incluye un campo secret (whsec_...) que solo se muestra una vez — guárdalo, lo necesitas para verificar firmas. Después, el handler en Express con el verificador del SDK:
import express from 'express';
import { WebhookVerifier } from '@beel_es/sdk';
const app = express();
const verifier = new WebhookVerifier(process.env.BEEL_WEBHOOK_SECRET!);
app.post(
'/webhooks/beel',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const event = verifier.verify(
req.body.toString('utf8'),
req.headers['beel-signature'] as string,
);
if (event.type === 'verifactu.status.updated') {
const { invoice_id, new_status, qr_url, invoice_hash, error_code } = event.data;
if (new_status === 'ACCEPTED') {
// La AEAT ha aceptado el registro: guarda qr_url e invoice_hash
console.log(`Factura ${invoice_id} aceptada`, qr_url, invoice_hash);
} else if (new_status === 'REJECTED') {
// Revisa error_code / error_message y corrige
console.error(`Factura ${invoice_id} rechazada: ${error_code}`);
}
}
res.status(200).send('OK');
} catch {
res.status(400).send('Invalid signature');
}
},
);Payload real del evento cuando la AEAT acepta:
{
"id": "6c0d4e5f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"type": "verifactu.status.updated",
"created_at": "2026-06-10T09:16:26Z",
"api_version": "2025-01",
"livemode": false,
"data": {
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"invoice_number": "FAC-2026/0042",
"verifactu_registration_id": "660e8400-e29b-41d4-a716-446655440001",
"previous_status": "PENDING",
"new_status": "ACCEPTED",
"qr_url": "https://sede.agenciatributaria.gob.es/Sede/verifactu?id=ABC123XYZ",
"qr_base64": "iVBORw0KGgoAAAANSUhEUgAAAMg...",
"invoice_hash": "B11F3A015173AD99075E2720F61E2DE1FF08CBFEDD85C6F73C77AD835301B2A3"
}
}Los estados posibles de new_status son PENDING, ACCEPTED, ACCEPTED_WITH_ERRORS, REJECTED y AEAT_SERVER_ERROR (en este último BeeL. reintenta automáticamente, sin que tú toques nada). Detalles a tener en cuenta:
- Tu endpoint debe responder
2xxen menos de 10 segundos; si no, BeeL. reintenta con backoff exponencial hasta un total de 5 intentos (el original + 4 reintentos). - Cada evento llega con el header
BeeL-Event-Id, idéntico en todos los reintentos — úsalo para deduplicar. - Las suscripciones están aisladas por entorno: las de sandbox solo reciben eventos de sandbox (
livemode: false). - ¿Aún no tienes endpoint? Apunta la suscripción a webhook.site y mira los payloads en directo.
#Paso 8: Checklist de paso a producción
Cuando el flujo funcione en sandbox, pasar a producción es una lista corta:
- ✓Sustituye la key beel_sk_test_ por una beel_sk_live_ (misma base URL, mismo código)
- ✓Completa tus datos fiscales y la serie de facturación en el panel de BeeL.
- ✓Crea la suscripción de webhook en producción (las de sandbox no reciben eventos live) y guarda su secret
- ✓Verifica la firma BeeL-Signature en todos los webhooks — nunca proceses un payload sin verificar
- ✓Deduplica eventos por BeeL-Event-Id y responde 2xx en menos de 10 segundos
- ✓Maneja el estado REJECTED: registra error_code y error_message de la AEAT y emite rectificativa si procede
- ✓No reintentes tú los envíos a la AEAT: BeeL. gestiona los reintentos respetando el encadenamiento
- ✓Guarda qr_url e invoice_hash de cada factura aceptada en tu base de datos
Eso es todo. Nada de certificados digitales, ni SOAP, ni esquemas XSD de la AEAT: BeeL. es el sistema informático de facturación certificado (con su declaración responsable publicada) y tu aplicación Node.js solo consume una API REST.
#Preguntas frecuentes
#¿Necesito un certificado digital para integrar Verifactu en Node.js?
No, si usas una API como la de BeeL. El certificado, la firma del registro, el encadenamiento de hashes y la comunicación con la AEAT los gestiona BeeL. como sistema informático de facturación. Tu aplicación solo necesita una API key con autenticación Bearer.
#¿Puedo probar la integración sin enviar nada a la AEAT?
Sí. Las API keys con prefijo beel_sk_test_ operan contra un sandbox aislado: las facturas no se envían a la AEAT, no consumen tu cuota mensual y puedes crear todas las que quieras. Es el entorno pensado para desarrollo y CI/CD.
#¿Qué pasa si la AEAT rechaza una factura?
Recibes un evento verifactu.status.updated con new_status: "REJECTED" y los campos error_code y error_message con el motivo exacto que devuelve la AEAT (por ejemplo, un NIF de emisor no registrado). La factura ya emitida no se borra: se corrige con una factura rectificativa según el caso.
#¿El webhook es obligatorio o puedo consultar el estado por API?
Puedes consultar GET /v1/invoices/{id} y leer verifactu.submission_status, pero el webhook es la forma recomendada: te evita hacer polling y te notifica en tiempo real cada cambio de estado. Los webhooks están disponibles con el plan Developer.
#¿Cuánto se tarda de verdad en integrar Verifactu con esta API?
El flujo de esta guía — cuenta, SDK, cliente, factura, emisión, PDF y webhook — se completa en unos 10 minutos en sandbox. Una integración productiva con manejo de errores y deduplicación de eventos suele cerrarse en una tarde, frente a las semanas/meses que lleva integrarse directamente contra los servicios SOAP de la AEAT.
#¿En qué se diferencia este tutorial del post sobre el SDK de TypeScript?
Este tutorial cubre el flujo Verifactu de punta a punta (registro AEAT, QR, hash, webhook de confirmación y paso a producción). Si quieres profundizar en el SDK en sí — builders, errores tipados, reintentos, idempotencia y el resto de recursos — tienes la guía completa en Facturación automática en Node.js con el SDK de TypeScript.
¿Listo para integrar? Crea tu cuenta gratuita, explora la API pública de BeeL. y la landing de la API Verifactu, o ve directo a la referencia: crear factura, emitir factura, autenticación y eventos de webhook.