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