BuildNexion

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=4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a
  • t — timestamp UNIX en segundos cuando se firmó.
  • v1 — HMAC-SHA256 hex de {t}.{raw_body} usando tu secret.

Cómo verificar

  1. Lee el body crudo (string) — no lo parsees a JSON todavía.
  2. Extrae t y v1 de la cabecera BuildNexion-Signature.
  3. Construye signed = `${t}.{body}`.
  4. Calcula expected = HMAC-SHA256(secret, signed).hexdigest().
  5. Compara expected con v1 en tiempo constante.
  6. Rechaza si la diferencia entre t y 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.id recibidos. 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-Delivery de cada petición — nos lo pides al abrir un ticket de debug.