Flujos y eventos
Bruxel ve lo que pasa adentro: la ficha, el envío del proveedor, la descripción publicada. Lo que pasa afuera —tu middleware llamó a SAP, el almacén dio de alta el material, la tienda publicó— solo lo sabe si tu sistema se lo reporta.
El scope
Reportar pide una llave con el scope events:write. Leer el recorrido de un producto pide catalog:read. La organización sale siempre de la llave: el cuerpo nunca la dice.
Reportar eventos
Manda hasta 100 eventos por llamada, de los productos que quieras. Cada evento es de un producto (por sku o por product_id) y dice qué sistema, qué tipo, cómo salió (started, ok, error o info) y cuándo pasó.
curl -X POST https://bruxel.ai/api/public/v1/events \
-H "Authorization: Bearer $BRUXEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"id": "mw-000123",
"sku": "BTL-1001",
"system": "sap",
"type": "material.created",
"status": "ok",
"occurred_at": "2026-10-05T18:04:11Z",
"duration_ms": 1840,
"summary": "Material creado: 4500012345",
"in_reply_to": "6f1c0e9a-7d2b-4c11-a4f0-91be2d07c3aa",
"request": { "method": "POST", "url": "https://sap.ejemplo/odata/A_Product", "body": { "Product": "BTL-1001" } },
"response": { "status": 201, "body": { "Product": "4500012345" } },
"link": "https://sap.ejemplo/material/4500012345",
"data": { "sap_code": "4500012345" }
}
]
}'La respuesta trae un renglón por evento, en el mismo orden: accepted (se guardó), duplicate (ya lo tenías: es el resultado original) o rejected (no se guardó, con su motivo).
{
"accepted": 1,
"duplicates": 0,
"rejected": 0,
"results": [
{
"index": 0,
"id": "mw-000123",
"outcome": "accepted",
"event_id": "…",
"product_id": "…",
"reason": null,
"detail": null,
"truncated": false
}
]
}Reglas del contrato
- Idempotencia: el id es tuyo y único por sistema. Repetir el mismo evento es seguro (duplicate); el mismo id con otro contenido sale rejected con id_conflict.
- Cada evento nombra su producto con sku o con product_id. Si mandas los dos, tienen que ser el mismo producto. Un producto borrado o de otra organización sale product_not_found.
- system va en minúsculas (sap, wms, tienda). Si no lo mandas, se usa el sistema conectado al que está ligada la llave (Cuenta → General → Sistemas conectados) o, si no está ligada, su nombre en minúsculas: «SAP middleware» se vuelve sap_middleware, salvo que ese sistema tenga su propia llave. Cualquier llave con events:write puede reportar por cualquier sistema si lo nombra: usa una llave por sistema.
- occurred_at es la hora del hecho, con zona (ISO 8601), y la pones tú; Bruxel guarda aparte la hora en que lo recibió. Se acepta hasta 5 minutos en el futuro y 30 días hacia atrás. Todo se ordena por occurred_at.
- Solo se agrega: un evento no se edita ni se borra. Un error se corrige mandando otro evento con el resultado nuevo.
- Hasta 16 KB por cuerpo (request.body y response.body), 4 KB de data y 300 caracteres de summary. Lo que pasa de eso se recorta y el evento queda marcado con truncated. El link va con https.
- No mandes tarjetas ni datos personales. Lo que parezca una credencial (Authorization, token, password, api_key, una llave conocida) se tapa antes de guardar, pero esa es la última red, no la primera. Los cuerpos se guardan 90 días; el evento, 13 meses.
Por qué se rechaza un evento
invalid— le falta un campo obligatorio, trae uno que no existe o tiene un valor con otra forma (el mensaje dice cuál).product_not_found— el producto no existe en tu catálogo o está en la papelera.id_conflict— ya reportaste un evento con ese id y otro contenido.occurred_at_future— occurred_at está más de 5 minutos en el futuro.occurred_at_too_old— occurred_at tiene más de 30 días.system_required— no mandaste system y la llave no lo dice: no está ligada a ningún sistema conectado, y su nombre no sirve de sistema o es el de un sistema que tiene su propia llave.link_invalid— link no es una dirección https.retry— no se pudo guardar ahora (un choque pasajero en la base): reintenta el MISMO evento.
Lo que mueve en un flujo
Un paso de un flujo se puede dar por hecho cuando un sistema reporta cierto evento. El paso se cumple con el último evento ok de ese sistema y tipo:
"done_when": { "event": { "system": "sap", "type": "material.created" } }Si el último evento de ese tipo es error, el paso queda con error y el recorrido se detiene hasta que llegue otro ok. Los eventos started e info se guardan en la bitácora pero no cumplen ni detienen nada.
El recorrido de un producto
GET /v1/products/:id/journey devuelve, por cada flujo activo en el que está el producto, el estado de cada paso y desde cuándo, la bitácora del recorrido y los eventos que reportaron los sistemas. Se recalcula al pedirlo.
curl https://bruxel.ai/api/public/v1/products/$PRODUCT_ID/journey \
-H "Authorization: Bearer $BRUXEL_API_KEY"
{
"product_id": "…",
"computed_at": "2026-10-05T18:05:02Z",
"runs": [
{
"flow_slug": "alta-producto",
"flow_name": "Alta de producto",
"status": "in_progress",
"steps": [
{ "key": "maestros", "name": "Datos maestros", "lane": "maestros", "state": "done", "since": "…", "late": false, "out_of_order": false },
{ "key": "sap_alta", "name": "Alta en SAP", "lane": "sap", "state": "done", "since": "2026-10-05T18:04:11Z", "late": false, "out_of_order": false },
{ "key": "tienda", "name": "Publicado", "lane": "tienda", "state": "active", "since": "2026-10-05T18:04:11Z", "late": false, "out_of_order": false }
],
"events": [ { "kind": "step", "step_key": "sap_alta", "to_state": "done", "cause": "event", "event_system": "sap", "event_type": "material.created", … } ],
"events_truncated": false
}
],
"reported": [ { "id": "mw-000123", "system": "sap", "type": "material.created", "status": "ok", … } ],
"reported_truncated": false
}Bruxel te avisa: te toca, se detuvo, terminó
Suscribe tu endpoint (Ajustes → Webhooks, o tu sistema en Sistemas conectados) a flow.step.ready, flow.run.blocked o flow.run.completed. Llegan agregados: uno por paso o por flujo cada vez que el motor mueve productos, con cuántos fueron y una muestra de hasta 20 con su SKU. Mientras uno no se entrega, lo que llega después no crea otro.
{
"event": "flow.step.ready",
"occurred_at": "2026-10-08T18:04:11Z",
"data": {
"flow": "alta-producto",
"step": "sap_alta",
"lane": "sap",
"system": "sap",
"count": 37,
"products": [ { "product_id": "…", "sku": "BTL-1001" }, … ]
}
}La muestra es una pista. Al recibir el aviso, lee la lista completa: la cola del paso (los que están en él, los que llevan más tiempo primero) o los recorridos detenidos o terminados del flujo, con cursor. Guarda el next_cursor y la próxima vez sigue desde ahí.
curl "https://bruxel.ai/api/public/v1/flows/alta-producto/steps/sap_alta/products?limit=200" \
-H "Authorization: Bearer $BRUXEL_API_KEY"
curl "https://bruxel.ai/api/public/v1/flows/alta-producto/runs?status=completed&cursor=$CURSOR" \
-H "Authorization: Bearer $BRUXEL_API_KEY"