Verificación HMAC
Cada webhook viene firmado con HMAC-SHA256. Tu servidor debe verificar la firma antes de procesar el evento — si no, cualquiera con tu URL podría enviarte payloads falsos.
La cabecera BuildNexion-Signature
Cada petición incluye:
http
BuildNexion-Signature: t=1716629400,v1=4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3at— timestamp UNIX en segundos cuando se firmó.v1— HMAC-SHA256 hex de{t}.{raw_body}usando tusecret.
Cómo verificar
- Lee el body crudo (string) — no lo parsees a JSON todavía.
- Extrae
tyv1de la cabeceraBuildNexion-Signature. - Construye
signed = `${t}.{body}`. - Calcula
expected = HMAC-SHA256(secret, signed).hexdigest(). - Compara
expectedconv1en tiempo constante. - Rechaza si la diferencia entre
ty la hora actual es mayor de 5 minutos (evita replays).
Si tu framework parsea el JSON antes de que tú lo veas (Express con
express.json(), Next.js Route Handlers…), debes leer el body crudo aparte. JSON.stringify(parsed) NO es equivalente — los espacios y el orden de claves cambian la firma.Helpers de verificación
import crypto from 'node:crypto'
export function verifyBuildNexionSignature({
payload, // string — raw body, antes de JSON.parse
signatureHeader,// header BuildNexion-Signature
secret, // whsec_… guardado al registrar el webhook
toleranceSeconds = 300,
}) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('=')),
)
const timestamp = Number(parts.t)
const provided = parts.v1
if (!timestamp || !provided) return false
// Rechaza payloads antiguos para evitar replays
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - timestamp) > toleranceSeconds) return false
const signed = `${timestamp}.${payload}`
const expected = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex')
// Comparación en tiempo constante para evitar timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(provided),
)
}Endpoint completo
import express from 'express'
import { verifyBuildNexionSignature } from './verify.js'
const app = express()
// IMPORTANTE: usa raw — express.json() ya parsea y rompe la firma
app.post(
'/buildnexion/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const ok = verifyBuildNexionSignature({
payload: req.body.toString('utf8'),
signatureHeader: req.headers['buildnexion-signature'],
secret: process.env.BUILDNEXION_WEBHOOK_SECRET,
})
if (!ok) return res.status(401).send('invalid signature')
const event = JSON.parse(req.body.toString('utf8'))
// ✅ responde rápido, encola el trabajo
queueJob(event)
res.status(200).send('ok')
},
)Rotar el secret
Si el secret se filtra, rótalo desde settings/integraciones/webhooks. Durante 24 h después de rotar aceptamos firmas con el secret antiguo o con el nuevo, para que despliegues sin downtime.
Buenas prácticas
- Idempotencia: guarda los
event.idrecibidos. Si llega dos veces (puede pasar con reintentos), no proceses dos veces. - Responde rápido: 200 antes de procesar. Encola el trabajo y procésalo asíncrono.
- HTTPS obligatorio. Rechazamos URLs
http://al registrar el webhook. - Logs: guarda el
BuildNexion-Deliveryde cada petición — nos lo pides al abrir un ticket de debug.