BuildNexion

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

  1. Prueba el webhook desde la app (Integraciones → Probar) para ver que la firma cuadra: llega un webhook.test.
  2. Sube una factura a una obra y comprueba que aparece en Holded.
  3. Si no llega, mira en la app el último error del webhook y los registros de tu servidor, buscando el X-BuildNexion-Delivery.