Tienes la extracción de facturas funcionando y los datos salen bien. El siguiente problema es de fontanería: cómo se entera tu sistema de que hay una factura nueva lista. Hay dos maneras, y una es claramente mejor.
Polling frente a webhooks
El polling consiste en preguntar cada X minutos: tu script llama a la API, pide la lista de facturas y compara con lo que ya tenía. Funciona, pero tiene tres inconvenientes: gastas llamadas para nada el 95 % de las veces, introduces un retraso igual al intervalo y tienes que llevar tú la cuenta de qué ya procesaste.
Un webhook le da la vuelta: en lugar de preguntar tú, el servicio te avisa. Registras una URL tuya y, cada vez que ocurre algo, recibes un POST con los datos del evento. Cero llamadas en vacío y aviso inmediato.
Los eventos que importan
- invoice.completed — una factura terminó de procesarse y sus datos están disponibles. Es el que usarás el 90 % de las veces.
- invoice.failed — el procesamiento falló (archivo ilegible, formato no soportado, un problema puntual). Suscribirte a este evento es lo que evita que una factura se pierda en silencio.
En InvoiceData das de alta cada webhook con su URL, los eventos a los que se suscribe y una descripción, y puedes activarlo o desactivarlo sin borrarlo. Cada intento de entrega queda registrado con su código de respuesta, así que puedes ver qué pasó y reintentar una entrega concreta si tu servidor estaba caído.
Cómo llega la notificación
La petición es un POST con el cuerpo en JSON y tres cabeceras propias:
POST https://tu-servidor.com/hooks/facturas
Content-Type: application/json
X-Invoice-Event: invoice.completed
X-Invoice-Timestamp: 1790000000
X-Invoice-Signature: sha256=8f4c...
{
"id": 1234,
"status": "completed",
"vendor_name": "Suministros Ebro SL",
"vendor_tax_id": "B50123456",
"invoice_number": "2026/0341",
"invoice_date": "2026-09-30",
"subtotal": 1000.00,
"tax_amount": 210.00,
"total": 1210.00,
"currency": "EUR"
}
Verificar la firma (esto no es opcional)
Tu URL es pública: cualquiera que la descubra puede enviarle un POST inventado. Por eso cada entrega va firmada. Al crear el webhook recibes un secreto; la firma es un HMAC-SHA256 calculado sobre el timestamp, un punto y el cuerpo exacto de la petición, usando ese secreto como clave.
En Python, con el cuerpo sin parsear (importante: firma sobre los bytes originales, no sobre el JSON reserializado):
import hmac, hashlib
def firma_valida(secreto, timestamp, cuerpo_bytes, cabecera):
base = (timestamp + ".").encode() + cuerpo_bytes
esperada = hmac.new(secreto.encode(), base, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + esperada, cabecera)
En Node:
const crypto = require('crypto');
function firmaValida(secreto, timestamp, cuerpoBuffer, cabecera) {
const base = Buffer.concat([Buffer.from(timestamp + '.'), cuerpoBuffer]);
const esperada = crypto.createHmac('sha256', secreto).update(base).digest('hex');
const a = Buffer.from('sha256=' + esperada);
const b = Buffer.from(cabecera || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Dos detalles que fallan siempre: comparar con comparación en tiempo constante (compare_digest o timingSafeEqual, nunca con el igual normal) y acceder al cuerpo en crudo. En Express necesitas el raw body; si dejas que el middleware de JSON lo parsee y luego lo vuelves a serializar, un espacio de diferencia tira la firma abajo.
Rechaza también los timestamps muy antiguos —más de unos minutos— para que nadie pueda reenviar una entrega capturada.
Responde rápido y trabaja después
La entrega tiene un tiempo de espera de 10 segundos. Si tu endpoint tarda más porque está insertando en el ERP, generando asientos o llamando a otra API, la entrega se marcará como fallida aunque tu proceso acabe bien.
El patrón correcto es: validar la firma, guardar el evento en una cola o una tabla, devolver 200 inmediatamente y procesar en segundo plano. Cualquier respuesta 2xx cuenta como entrega correcta; el resto queda registrado como error para que puedas reintentarlo.
Hazlo idempotente
Un webhook puede llegarte dos veces: por un reintento manual, por un corte de red justo al responder o por un despliegue a medias. Si cada entrega crea un asiento contable, el día que se duplique tendrás un problema difícil de encontrar.
La solución es de una línea: guarda el identificador de la factura junto con tu registro y, antes de insertar, comprueba si ya lo tienes. Si ya está, responde 200 y no hagas nada más.
Errores frecuentes al montar el receptor
- URL sin HTTPS. Estás recibiendo datos fiscales de tus proveedores; que viajen cifrados no es negociable.
- Devolver 500 por un dato raro. Si una factura viene sin número, no revientes: guarda el evento y márcalo para revisión.
- No mirar nunca el historial de entregas. Un webhook que lleva dos semanas devolviendo 404 porque cambiaste la ruta es una fuga silenciosa de facturas.
- Suscribirse solo a invoice.completed. Sin el evento de fallo, las facturas que no se procesan desaparecen de tu radar.
Cuándo usar webhooks y cuándo no
Si tienes un servidor propio con una URL accesible desde internet, los webhooks son la mejor opción. Si no lo tienes, hay alternativas perfectamente válidas: montar el flujo en n8n o llamar a la API desde un proceso programado, o directamente recibir las facturas por email y exportar los datos cuando los necesites.
Para el resto de la integración —autenticación, endpoints y campos que devuelve cada respuesta— tienes el detalle en la página de la API y en la guía para desarrolladores. Si lo que quieres es que los datos acaben en tu programa de contabilidad, mira también cómo conectar la extracción con Holded, Sage o A3.
¿Quieres que lo veamos sobre tu caso concreto? Pide una demo y lo repasamos con tu flujo real delante.