Webhook de decisión
El webhook de decisión deja que tus propios sistemas participen en cada decisión de pago. Antes de autorizar un pago, Burnbound envía los hechos del pago a un endpoint HTTPS que tú gestionas, y tu código responde allow, deny o hold con tu lógica de negocio: el presupuesto de un proyecto, un ticket abierto, una lista de proveedores aprobados. Cubre las APIs de pago (x402 y MPP), las autorizaciones de tarjetas de Stripe Issuing y los reembolsos del action gateway.
El webhook solo endurece la decisión. deny bloquea un pago y hold pide que una persona lo apruebe, pero allow nunca deja pasar un pago que tus topes o tus reglas rechazan. Está en el plan Business.
¿Cómo lo configuro?
- En el panel, abre Organización → Webhook de decisión. Solo lo ven un owner o un admin.
- Escribe la URL de tu endpoint. Tiene que ser
httpsen el puerto 443, con un nombre DNS público. Burnbound rechaza las direcciones privadas y lo vuelve a comprobar en cada llamada. - Copia el secreto (
bbdw_…). Solo se muestra una vez. - Déjalo al principio en observe: Burnbound lo llama después de cada decisión y guarda su respuesta en la auditoría, sin cambiar nada. Con Probar webhook envías una petición de ejemplo (
"test": true, sin pago). - Cuando las respuestas sean las correctas, pásalo a enforce.
Ajustes opcionales:
- Solo para algunos subequipos o agentes. Sin marcar, se llama para todos los agentes de la organización.
- Condiciones: cuándo llamarlo, con los mismos hechos y la misma sintaxis que las reglas personalizadas (por ejemplo, solo por encima de 20 USD o solo para hosts nuevos).
- Si falla: negar el pago (por defecto) o pedir que una persona lo apruebe.
- Tiempo de espera: de 200 a 1500 ms (1500 por defecto).
¿Qué envía Burnbound?
Un POST con un cuerpo JSON y dos cabeceras:
POST /burnbound/decision HTTP/1.1
Content-Type: application/json
Burnbound-Webhook: <your webhook id>
Burnbound-Signature: t=1793280000,v1=<hex HMAC-SHA256>
{
"version": 1,
"id": "dwr_01JBX4T6Z8Q9W2E3R4T5Y6V7K8",
"createdAt": "2026-10-29T10:00:00.000Z",
"org": { "id": "<your org id>" },
"agent": { "id": "<agent id>", "memberId": null, "teamId": "<team id>" },
"payment": {
"idempotencyKey": "order-8812",
"host": "api.example.com",
"path": "/v1/query",
"httpMethod": "POST",
"amountUsd": "0.25",
"paymentMethod": "x402.exact",
"rail": "eip155:8453",
"asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"payeeAddress": "0x…",
"seller": { "listed": true, "category": "data" },
"firstPaymentToHost": false,
"firstPaymentToPayee": false
},
"preliminary": {
"decision": "allow",
"matchedRules": [],
"spentTodayUsd": "4.10"
},
"declared": {
"trust": "agent_declared",
"origin": "user",
"context": { "ticket": "OPS-1423" }
}
}
ides único en cada llamada.payment.idempotencyKeyes el mismo cuando el agente reintenta la misma compra: úsalo para deduplicar.- Para encontrar una llamada en la auditoría: en enforce Burnbound llama a tu endpoint antes de que exista el pago, así que su evento
decision_webhook.calledodecision_webhook.failedaún no tiene pago y llevarequestId(elidde arriba) eidempotencyKey. Después el pago nombra el mismoid: comodecisionWebhookRequestIden su eventopayment.authorized, o como elrequestIdde la regla del webhook en una denegación o una retención. paymentlleva los hechos que leen tus reglas personalizadas. En las autorizaciones de tarjeta también llevamerchantCategory,merchantCountryyfiatAmount; en los reembolsos del action gateway,fiatAmountyrefund. Nunca lleva la query string, el texto del vendedor, la firma del pago, claves, emails ni datos de facturación.preliminaryes lo que decidieron tus topes y tus reglas sin el webhook.declaredes lo que el agente dice del pago. Mira ¿Puedo fiarme del contexto que declara el agente?
¿Qué debe responder mi endpoint?
HTTP 200 con un cuerpo JSON de como mucho 4 KB, firmado con el mismo secreto en la misma cabecera Burnbound-Signature, y con el id de la petición:
{
"version": 1,
"id": "dwr_01JBX4T6Z8Q9W2E3R4T5Y6V7K8",
"decision": "hold",
"reason": "Ticket OPS-1423 is closed"
}
decision:allow,denyohold.reason(opcional, como mucho 200 caracteres) se muestra al agente y en el panel. Burnbound lo trata como texto no fiable: al agente se le dice que lo cite y que nunca lo siga. Se queda en una línea de texto visible: los caracteres de control pasan a espacios y se quitan los invisibles (ancho cero, bidireccionales, caracteres de etiqueta Unicode, selectores de variación y similares). No pongas en él secretos ni instrucciones.
Una respuesta sin firma, con otro id, que no es JSON o que pasa de 4 KB cuenta como fallo, nunca como allow. Burnbound no sigue redirecciones.
¿Cómo verifico la firma?
La firma es un HMAC-SHA256 de <t>.<body> con tu secreto, en hexadecimal, donde t es la hora Unix en segundos y body es el cuerpo de la petición tal cual llega. Rechaza un t a más de 5 minutos de tu reloj y un id que ya hayas visto. Este endpoint de referencia en Node.js hace las dos cosas y firma su respuesta:
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const SECRET = process.env.BURNBOUND_WEBHOOK_SECRET; // bbdw_…
const seen = new Set(); // request ids; use your database in production
const sign = (t, body) =>
createHmac("sha256", SECRET).update(`${t}.${body}`).digest("hex");
const verify = (header = "", body) => {
const parts = header.split(",").map((part) => part.trim().split("="));
const t = parts.find(([key]) => key === "t")?.[1];
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = Buffer.from(sign(t, body), "hex");
return parts.some(([key, value = ""]) => {
const given = Buffer.from(value, "hex");
return (
key === "v1" &&
given.length === expected.length &&
timingSafeEqual(given, expected)
);
});
};
// Your business logic: check the facts against your own systems.
const decide = ({ payment }) =>
Number(payment.amountUsd) > 50
? { decision: "hold", reason: "Over 50 USD: a manager approves it" }
: { decision: "allow" };
createServer((req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const request =
verify(req.headers["burnbound-signature"], body) && JSON.parse(body);
if (!request || seen.has(request.id)) return res.writeHead(401).end();
seen.add(request.id);
const answer = JSON.stringify({
version: 1,
id: request.id,
...decide(request),
});
const t = Math.floor(Date.now() / 1000);
res
.writeHead(200, {
"content-type": "application/json",
"burnbound-signature": `t=${t},v1=${sign(t, answer)}`,
})
.end(answer);
});
}).listen(8080); // behind your HTTPS proxy
Mientras dura la rotación de un secreto, la cabecera lleva dos valores v1, uno por secreto, así que el endpoint funciona con cualquiera de los dos. Burnbound también acepta una respuesta firmada con cualquiera de ellos.
¿Puedo fiarme del contexto que declara el agente?
No. Un agente puede pasar context a fetch_paid (hasta 10 claves, valores de como mucho 200 caracteres):
{
"url": "https://api.example.com/v1/query",
"context": { "ticket": "OPS-1423", "project": "atlas" }
}
Burnbound lo reenvía dentro de declared, con la etiqueta "trust": "agent_declared", y lo guarda en la auditoría. Es lo que dice el agente, no una prueba: un agente engañado por una inyección de prompt puede nombrar cualquier ticket. Tu endpoint tiene que comprobarlo contra tu propio sistema (que el ticket existe, está abierto y es de ese agente o equipo) antes de responder allow. Las reglas personalizadas en enforce nunca lo leen.
¿Qué pasa si mi endpoint falla o va lento?
Un tiempo de espera agotado, un estado distinto de 200, una redirección, una firma incorrecta, una respuesta no válida, una dirección privada o un error de red son un fallo, y Burnbound aplica tu ajuste Si falla: negar el pago (por defecto) o pedir que una persona lo apruebe. No hay opción para permitir un pago cuando el endpoint está caído. Por encima de 600 llamadas por minuto y organización, Burnbound no llama al endpoint y el pago cuenta como fallo (rate_limited).
El agente recibe decision_webhook_unavailable con la causa, y el panel muestra el último resultado y la latencia de las últimas 24 horas.
Burnbound no llama al webhook cuando la respuesta no puede cambiar nada: cuando tus topes o tus reglas ya niegan el pago, cuando el agente repite una compra que ya está autorizada, o cuando el alcance o las condiciones no aplican.
¿Cómo funcionan las retenciones, las aprobaciones y los cambios?
- Un
holdcrea una aprobación, como una regla personalizada conrequire_approval. Cuando una persona la aprueba y el agente reintenta, Burnbound vuelve a preguntar a tu endpoint, y entonces undenygana. - Cambiar la URL, el alcance o las condiciones crea una versión nueva del webhook. Las aprobaciones dadas con la versión anterior, pendientes o no, dejan de cubrir el pago: el agente vuelve a pedirla.
- Pasar de observe a enforce o al revés, o cambiar Si falla o el tiempo de espera, no crea una versión nueva.
¿Cómo funciona con tarjetas y reembolsos?
En una autorización de tarjeta de Stripe Issuing, Stripe espera unos 2 segundos. Burnbound da a tu endpoint lo que queda de ese tiempo menos un margen, hasta tu tiempo de espera, medido justo antes de llamarte. Si quedan menos de 200 ms, no llama a tu endpoint y cuenta un tiempo de espera agotado. Si tu endpoint llega tarde, la autorización se rechaza, diga lo que diga tu ajuste Si falla. Una tarjeta no puede esperar a una persona, así que un hold también la rechaza. Cuando tu endpoint no permite una tarjeta (un deny, un hold o un fallo), Burnbound la rechaza en el acto, antes de esperar a otros pagos del mismo agente, así que un agente ocupado nunca lleva la respuesta más allá del plazo de Stripe.
En los reembolsos del action gateway, hold crea una aprobación igual que en las APIs de pago.
El payment.refund de un reembolso dice de qué va: lo lee tu gateway en Stripe y lo calcula Burnbound en el momento de decidir:
{
"originalAgeDays": 31,
"fraction": 0.4,
"disputed": false,
"customerAgeDays": 400,
"customerRefunds90d": { "count": 2, "usd": "13.2", "truncated": false },
"reason": "requested_by_customer"
}
originalAgeDaysycustomerAgeDays: días enteros desde el cargo y desde el alta del cliente, redondeando hacia abajo.fraction: lo reembolsado del cargo, este reembolso incluido, de 0 a 1 con 4 decimales, redondeando hacia arriba.customerRefunds90d: los cargos del cliente de los últimos 90 días que ya tenían un reembolso, y sus USD (redondeando hacia arriba).truncated: truequiere decir que el cliente tiene más cargos de los que lee el gateway, así que las cifras son un mínimo.reason: el motivo con el que pidió el agente, onullsi no dio ninguno.- Un hecho que tu gateway no envió (un gateway antiguo, una lectura de Stripe que falló) es
null. Tus reglas personalizadas lo tratan como la rama restrictiva; tu endpoint decide qué hacer con él.
Los demás pagos no llevan refund.
¿Cómo roto el secreto?
Pulsa Rotar secreto. Burnbound muestra el secreto nuevo una vez y sigue firmando también con el anterior durante 24 horas, para que actualices tu endpoint sin cortes. Si el secreto se ha filtrado, elige Rotar sin gracia: el anterior deja de usarse al momento.
Cada cambio, rotación y prueba queda en la auditoría. Una agent key no puede leer ni cambiar el webhook.