Recibe un aviso firmado en tu servidor cada vez que cambia algo en tu cuenta, sin consultar la API cada poco.
Qué son
Un webhook es una dirección de tu servidor a la que BMC envía una petición POST cuando ocurre un
evento: una factura emitida, un cobro registrado, un sobre de firma completado. Cada aviso es fino:
lleva el tipo de evento, el identificador del recurso y unos pocos campos no personales. Para el detalle, pide el
recurso a la API con ese identificador.
Con webhooks no necesitas consultar la API cada pocos minutos para saber qué cambió, y gastas menos cupo
(Límites de uso). Hay 44 eventos en 9 áreas.
Cómo se crea un webhook
Hay dos vías:
En el área de cliente, en Desarrolladores › Webhooks: indica un nombre, la URL de destino y los eventos.
Con la API, con POST https://app.bm.consulting/api/v1/webhooks/subscriptions.
Crear una suscripción
curl -X POST "https://app.bm.consulting/api/v1/webhooks/subscriptions" \
-H "Authorization: Bearer $BMC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Facturas emitidas hacia mi ERP",
"url": "https://erp.example.com/webhooks/bmc",
"events": ["invoice.issue", "invoice.pay"]
}'
Respuesta (201)
{
"id": "…",
"name": "Facturas emitidas hacia mi ERP",
"url": "https://erp.example.com/webhooks/bmc",
"events": ["invoice.issue", "invoice.pay"],
"secret": "whsec_…",
"message": "Save the secret now: it will not be shown again. …"
}
Guarda el secreto ahora. El campo secret (whsec_…) solo se muestra al
crear la suscripción y al rotarlo. BMC lo conserva cifrado y no puede volver a enseñártelo.
Requisitos
La URL debe ser https y resolver a una dirección pública. No se admiten credenciales en la URL, nombres internos ni direcciones IP privadas.
El token necesita el permiso developers:webhooks.create y, además, el permiso de lectura de cada evento al que se suscribe (columna «Permiso exigido» del catálogo). Si falta alguno, la respuesta es 403 y lista los permisos que faltan.
Un token ligado a una cuenta solo recibe eventos de esa cuenta.
Rutas de la API de webhooks
Ruta
Qué hace
Permiso
GET /webhooks/events
Catálogo de eventos que tu token puede suscribir (?lang=es).
developers:webhooks.read
GET /webhooks/subscriptions
Lista tus suscripciones.
developers:webhooks.read
POST /webhooks/subscriptions
Crea una suscripción y devuelve el secreto.
developers:webhooks.create
GET /webhooks/subscriptions/{id}
Consulta una suscripción.
developers:webhooks.read
PATCH /webhooks/subscriptions/{id}
Cambia el nombre, la URL, los eventos o el estado activo.
developers:webhooks.update
DELETE /webhooks/subscriptions/{id}
Elimina la suscripción.
developers:webhooks.delete
POST /webhooks/subscriptions/{id}/test
Envía un evento de prueba webhook.test con un solo intento.
developers:webhooks.test
POST /webhooks/subscriptions/{id}/rotate-secret
Genera un secreto nuevo; el anterior deja de servir.
developers:webhooks.update
GET /webhooks/subscriptions/{id}/deliveries
Registro de entregas con su estado.
developers:webhooks.read
POST /webhooks/deliveries/{id}/redeliver
Reenvía una entrega concreta.
developers:webhooks.update
Qué recibe tu servidor
Cuerpo
JSON con Content-Type: application/json. Las claves llegan siempre en el mismo orden.
Identificador del evento. Úsalo para descartar duplicados.
X-BMC-Webhook-Delivery
Identificador de esta entrega concreta.
X-BMC-Webhook-Date
Fecha de este intento, en ISO 8601.
X-BMC-Webhook-Account-Id
Cuenta a la que pertenece el evento; vacía si ninguna.
X-BMC-Webhook-Version
Versión del contrato: v1.
X-BMC-Webhook-Signature
Firma: t=<unix>,sha256=<hex>.
Verifica la firma
Cualquiera que conozca tu URL puede enviarle peticiones. Comprueba siempre la firma antes de fiarte del contenido:
Lee la cabecera X-BMC-Webhook-Signature y separa t (segundos Unix) y sha256.
Rechaza la entrega si t se aleja más de 300 segundos de tu reloj. Así una entrega capturada no se puede reenviar más tarde.
Calcula un HMAC-SHA256 con tu secreto completo (whsec_…) como clave sobre el texto {t}.{cuerpo}, donde el cuerpo son los bytes exactos recibidos, sin volver a serializar el JSON.
Compara con sha256 en tiempo constante.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.BMC_WEBHOOK_SECRET; // whsec_…
const TOLERANCE = 300; // segundos
app.post("/webhooks/bmc", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-BMC-Webhook-Signature") ?? "";
const parts = Object.fromEntries(
header.split(",").map((kv) => {
const i = kv.indexOf("=");
return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE) {
return res.sendStatus(400);
}
// req.body es un Buffer con los bytes exactos recibidos
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.`).update(req.body).digest("hex");
const received = parts.sha256 ?? "";
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!valid) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
const eventId = req.get("X-BMC-Webhook-Id");
// 1. Si ya procesaste eventId, responde 200 sin hacer nada.
// 2. Si no, guarda eventId y encola el trabajo.
res.sendStatus(200);
});
Python (Flask)
import hashlib
import hmac
import os
import time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["BMC_WEBHOOK_SECRET"] # whsec_…
TOLERANCE = 300 # segundos
def verify(payload: bytes, header: str) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, received = int(parts["t"]), parts["sha256"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE:
return False
expected = hmac.new(SECRET.encode(), f"{t}.".encode() + payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)
@app.post("/webhooks/bmc")
def bmc_webhook():
payload = request.get_data() # bytes exactos, antes de parsear
if not verify(payload, request.headers.get("X-BMC-Webhook-Signature", "")):
return "", 400
event_id = request.headers["X-BMC-Webhook-Id"]
# 1. Si ya procesaste event_id, responde 200 sin hacer nada.
# 2. Si no, guarda event_id y encola el trabajo.
return "", 200
Respuesta esperada y reintentos
Responde con cualquier código 2xx para confirmar la recepción. Hazlo rápido: BMC espera como máximo 10 segundos. Si el trabajo es largo, guarda el evento, responde y procésalo después.
Cualquier otra respuesta se considera un fallo, incluidas las redirecciones: BMC no las sigue. También es un fallo no responder a tiempo o no poder conectar.
Tras un fallo, BMC reintenta con espera creciente: 1 min, 5 min, 30 min, 2 h, 12 h. Son 6 intentos en total, contando el primero. Pasados todos, la entrega queda como fallida.
Las entregas y su estado se ven en Desarrolladores › Webhooks y en GET /webhooks/subscriptions/{id}/deliveries. Una entrega fallida se puede reenviar a mano.
Idempotencia
Con los reintentos, tu servidor puede recibir el mismo evento más de una vez. X-BMC-Webhook-Id (y el
campo id del cuerpo) es el mismo en todos los intentos y reenvíos del evento: guárdalo al procesarlo y,
si llega otra vez, responde 200 sin repetir el trabajo. X-BMC-Webhook-Delivery cambia en cada entrega y sirve para seguirla en el registro.
El orden de llegada no está garantizado. Si el orden importa, compara occurred_at o consulta el estado
actual del recurso en la API.
Prueba y rotación del secreto
Enviar prueba. Desde el área de cliente o con POST /webhooks/subscriptions/{id}/test se envía un evento webhook.test con un solo intento y se muestra el resultado. No es un evento suscribible.
Rotar el secreto. Genera uno nuevo; el anterior deja de servir. Actualiza tu servidor con el valor nuevo y hazlo si sospechas que se ha expuesto.
Pausar. Una suscripción desactivada no recibe eventos.
Catálogo de eventos
Cada evento exige el permiso de lectura del recurso al que se refiere, el mismo que pide la ruta de la API que lo
devuelve. Los nombres de los permisos están en el catálogo de permisos.
Una suscripción solo puede incluir los eventos cuyo permiso tenga el token.