Sincronizar facturas con Holded
Cada factura de compra que entra en BuildNexion se crea como factura de gasto en Holded: un webhook invoice.created y una llamada a la API de Holded.
¿Te basta con lo estándar? BuildNexion ya se conecta con Holded sin programar nada: en Configuración → Integraciones → Holded pegas tu API key de Holded y las facturas se envían solas. Este ejemplo es para cuando necesitas tu propia lógica (otra cuenta contable, filtrar por obra, avisar a alguien…).
Qué necesitas
- Una API key de BuildNexion con el permiso
write:webhooks(para registrar el webhook). - Una API key de Holded (en Holded: Configuración → Desarrolladores).
- Un servidor con una URL pública HTTPS.
1. Registrar el webhook
bash
curl -X POST https://app.buildnexion.com/api/v1/webhooks \
-H "Authorization: Bearer $BUILDNEXION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-servidor.es/buildnexion/holded",
"events": ["invoice.created"]
}'Guarda el secret de la respuesta: lo necesitas para verificar la firma.
2. Recibir el aviso
Verifica la firma sobre el cuerpo en crudo, responde enseguida y no proceses dos veces la misma entrega:
js
import express from 'express'
import { firmaValida } from './firma.js' // ver "Verificar la firma"
const app = express()
const yaProcesadas = new Set() // en producción, una tabla
app.post('/buildnexion/holded', express.raw({ type: 'application/json' }), async (req, res) => {
if (!firmaValida(req.body, req.get('X-BuildNexion-Signature'), process.env.BUILDNEXION_WEBHOOK_SECRET)) {
return res.status(401).send('firma no válida')
}
res.sendStatus(200) // responde enseguida y procesa después
const entrega = req.get('X-BuildNexion-Delivery')
if (yaProcesadas.has(entrega)) return
yaProcesadas.add(entrega)
const evento = JSON.parse(req.body.toString('utf8'))
if (evento.event === 'invoice.created') await enviarAHolded(evento.data)
})3. Crear la factura en Holded
invoice.created ya trae el proveedor (con su NIF), la base, el IVA, el total y las líneas, así que no hace falta pedir nada más a BuildNexion:
js
async function enviarAHolded(factura) {
const res = await fetch('https://api.holded.com/api/invoicing/v1/documents/purchase', {
method: 'POST',
headers: { key: process.env.HOLDED_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
// Holded localiza (o crea) el proveedor por su NIF y su nombre
contactCode: factura.supplier.cif ?? undefined,
contactName: factura.supplier.name,
date: Math.floor(new Date(factura.date).getTime() / 1000),
desc: `Factura ${factura.number}`,
externalReference: `bn:${factura.id}`,
items: [{
name: `Factura ${factura.number}`,
units: 1,
// Importes en euros. Si la lectura no sacó la base, se usa el total
subtotal: factura.base_imponible ?? factura.amount,
tax: factura.base_imponible != null ? (factura.iva_porcentaje ?? 21) : 0,
}],
}),
})
if (!res.ok) throw new Error(`Holded respondió ${res.status}: ${await res.text()}`)
}Los campos de Holded son orientativos: compruébalos en su documentación. Si Holded falla, tu endpoint ya respondió 200 y BuildNexion no reintentará: guarda el error y reinténtalo tú.
4. Probar
- Prueba el webhook desde la app (Integraciones → Probar) para ver que la firma cuadra: llega un
webhook.test. - Sube una factura a una obra y comprueba que aparece en Holded.
- Si no llega, mira en la app el último error del webhook y los registros de tu servidor, buscando el
X-BuildNexion-Delivery.