BuildNexion

Sincronizar facturas con Holded

Tutorial completo: cada factura nueva en BuildNexion se replica automáticamente como factura de gasto en Holded. Webhook entrante + llamada a la API de Holded.

Qué necesitas

  • API key de BuildNexion con scope read:invoices + manage:webhooks.
  • API key de Holded (encontrarás cómo en su panel).
  • Un servidor con endpoint público HTTPS (Vercel, Cloud Run, Fly, Render…).

Paso 1 — Registrar el webhook

Suscríbete a invoice.created apuntando a tu servidor:

bash
curl -X POST https://api.buildnexion.com/v1/webhooks \
  -H "Authorization: Bearer bn_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url":    "https://mi-app.com/buildnexion/holded-sync",
    "events": ["invoice.created"]
  }'

Guarda el secret que devuelve. Lo usarás para verificar las firmas.


Paso 2 — Mapear proveedores entre los dos sistemas

BuildNexion y Holded identifican proveedores por NIF. Antes de empujar facturas, asegúrate de que cada proveedor existe en Holded. Idempotente: si ya existe, Holded devuelve el ID existente.

js
async function upsertHoldedContact({ name, nif, email }) {
  // 1. ¿Existe ya?
  const search = await fetch(
    `https://api.holded.com/api/invoicing/v1/contacts?code=${nif}`,
    { headers: { key: process.env.HOLDED_API_KEY } },
  ).then(r => r.json())

  if (Array.isArray(search) && search[0]?.id) return search[0].id

  // 2. Crear
  const created = await fetch(
    'https://api.holded.com/api/invoicing/v1/contacts',
    {
      method: 'POST',
      headers: {
        key: process.env.HOLDED_API_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name,
        code:        nif,
        email,
        type:        'supplier',
        isperson:    false,
      }),
    },
  ).then(r => r.json())
  return created.id
}

Paso 3 — Endpoint del webhook

js
import express from 'express'
import { verifyBuildNexionSignature } from './verify.js'

const app = express()

app.post(
  '/buildnexion/holded-sync',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // 1. Verifica firma
    const raw = req.body.toString('utf8')
    const ok  = verifyBuildNexionSignature({
      payload:         raw,
      signatureHeader: req.headers['buildnexion-signature'],
      secret:          process.env.BUILDNEXION_WEBHOOK_SECRET,
    })
    if (!ok) return res.status(401).send('invalid signature')

    // 2. Responde rápido — encola el trabajo
    res.status(200).send('ok')

    const event = JSON.parse(raw)
    if (event.type !== 'invoice.created') return

    await syncInvoiceToHolded(event.data.object)
  },
)

Paso 4 — Crear la factura en Holded

js
async function syncInvoiceToHolded(invoice) {
  // 1. Pide los datos del proveedor a BuildNexion
  const providerRes = await fetch(
    `https://api.buildnexion.com/v1/providers/${invoice.provider_id}`,
    { headers: { Authorization: `Bearer ${process.env.BUILDNEXION_API_KEY}` } },
  )
  const provider = await providerRes.json()

  // 2. Upsert del contacto en Holded
  const contactId = await upsertHoldedContact({
    name:  provider.name,
    nif:   provider.nif,
    email: provider.email,
  })

  // 3. Crea la factura de gasto (purchase) en Holded
  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({
      contactId,
      docNumber:   invoice.invoice_number,
      date:        Math.floor(new Date(invoice.issue_date).getTime() / 1000),
      // Holded espera importes en euros (decimales), BuildNexion devuelve céntimos
      items: [{
        name:     `Factura ${invoice.invoice_number}`,
        units:    1,
        subtotal: invoice.subtotal_amount / 100,
        tax:      Math.round((invoice.tax_amount / invoice.subtotal_amount) * 100),
      }],
    }),
  })
}
Añade idempotencia: guarda event.id en una tabla processed_events y descarta los duplicados. Los reintentos del webhook usan siempre el mismo ID.

Paso 5 — Probar end-to-end

  1. Crea una factura de prueba en BuildNexion (con una API key bn_test_).
  2. Comprueba los logs de tu endpoint — debería llegar el evento.
  3. Verifica que la factura aparece en Holded.
  4. Si algo falla, revisa BuildNexion-Delivery en los logs y ábrelo en settings/integraciones/webhooks → Histórico para ver el payload exacto, los reintentos y la respuesta de tu servidor.