bruxel.ai

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
    }
  ]
}
Un evento mal formado no tumba el lote: sale rejected en su renglón y los demás se guardan. La llamada contesta 422 solo si el cuerpo no es un objeto con events o trae más de 100 eventos, y 413 si pasa de 1 MB.

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
}
Sin cuerpos ni personas: lo que se mandó y lo que se contestó se ve dentro de Bruxel, y la bitácora dice qué pasó y por qué camino, no quién. Con los flujos apagados contesta 404.

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"
Activar un flujo despierta una vez por cada paso que tiene productos: es justo cuando tu sistema tiene que leer su cola. Con el scope catalog:read basta para leerla.
Flujos y eventos — Bruxel