REST APISocket.IOControlware Business

Documentación de Controlware

Integra dispositivos y sistemas externos con la API de Controlware. Administra recursos mediante REST y consume telemetría y alertas en tiempo real mediante WebSocket.

Ver llms.txt

API REST

Gestión y consulta de recursos.

Tiempo real

Telemetría y alertas en vivo.

Aislamiento seguro

Datos segmentados por tenant.

Primeros pasos

Inicio rápido

Todas las rutas utilizan JSON y parten de la siguiente URL base.

BASE URLhttps://api.controlware-iot.com/api

Primera solicitud

curl --request GET \
  --url https://api.controlware-iot.com/api/devices?tenantId=<TENANT_ID> \
  --header 'Authorization: Bearer <TOKEN>' \
  --header 'Accept: application/json'

Seguridad

Autorización

Token de API

Recomendado para integraciones. Es persistente, revocable y está ligado a un tenant.

Administrar tokens

Cookie de sesión

La aplicación web utiliza una cookie HttpOnly. El navegador la envía automáticamente.

Bearer de sesión

Para aplicaciones móviles, utiliza el accessToken devuelto por /auth/login.

Aislamiento por tenant. Un token sólo permite acceder a los dispositivos y la telemetría de su tenant. Para operar sobre otro tenant, genera un token específico o cambia el tenant activo.

Formato de error

{ "statusCode": 401, "error": "Unauthorized", "message": "..." }

Referencia

Endpoints REST

Operaciones disponibles para autenticación, administración de tenants, tokens, dispositivos y alertas.

Autenticación

Login/registro de sesión. Devuelven el usuario + tokens y, para la web, fijan una cookie HttpOnly de sesión.

5 endpoints
POST/auth/registerPública

Crea usuario + empresa (tenant) y abre sesión. La contraseña debe tener mínimo 8 caracteres.

Cuerpo de la solicitud

{
  "email": "[email protected]",
  "password": "********",
  "name": "Ana",
  "company": "Mi Empresa"
}

Respuesta

{
  "user": { "id": "...", "email": "...", "name": "...",
            "activeTenant": { "id": "...", "name": "...", "slug": "..." },
            "role": "owner" },
  "accessToken": "<jwt>",
  "refreshToken": "<opaco>",
  "expiresIn": 900,
  "tokenType": "Bearer"
}
POST/auth/loginPública

Autentica por email/contraseña. Misma respuesta que register.

Cuerpo de la solicitud

{ "email": "[email protected]", "password": "********" }
POST/auth/refreshPública

Rota el refresh token (revoca el actual y emite uno nuevo) y devuelve un nuevo accessToken.

Cuerpo de la solicitud

{ "refreshToken": "<opaco>" }
POST/auth/logoutPública

Cierra sesión: limpia la cookie y revoca el refresh token si se envía. Responde 204.

Cuerpo de la solicitud

{ "refreshToken": "<opaco>" }  // opcional
GET/auth/meToken / Cookie

Devuelve el usuario autenticado y su tenant activo.

Respuesta

{ "id": "...", "email": "...", "name": "...",
  "activeTenant": { "id": "...", "name": "...", "slug": "..." },
  "role": "owner" }

Tenants

Un usuario puede pertenecer a varios tenants (de su empresa). El tenant activo vive en el JWT.

2 endpoints
GET/membershipsToken / Cookie

Lista los tenants a los que el usuario tiene acceso.

Respuesta

[ { "role": "owner",
    "tenant": { "id": "...", "name": "Avimex", "slug": "avimex" } } ]
POST/tenants/switchToken / Cookie

Cambia el tenant activo de la sesión y reemite la cookie/JWT con el nuevo tenant.

Cuerpo de la solicitud

{ "tenantId": "tnt_avimex" }

Respuesta

{ "user": { "...": "...", "activeTenant": { "...": "..." } } }

Tokens de API

Tokens persistentes (365 días) y revocables, ligados a un tenant. Se gestionan en la página Tokens de API.

3 endpoints
GET/api-tokensToken / Cookie

Empresa, tenant activo, tenants seleccionables y tokens existentes (cada uno con su tenant).

Respuesta

{
  "company": { "id": "...", "name": "Controlware" },
  "activeTenantId": "tnt_avimex",
  "tenants": [ { "id": "...", "name": "Avimex", "slug": "avimex" } ],
  "tokens": [ { "id": "...", "name": "backend ERP",
                "tenantId": "tnt_avimex", "tenantName": "Avimex",
                "companyName": "Controlware", "createdAt": "..." } ]
}
POST/api-tokensToken / Cookie

Genera un token. tenantId es opcional: si se omite usa el tenant activo. El token sólo se devuelve una vez.

Cuerpo de la solicitud

{ "name": "backend ERP", "tenantId": "tnt_avimex" }

Respuesta

{ "id": "...", "name": "backend ERP",
  "companyName": "Controlware",
  "tenantId": "tnt_avimex", "tenantName": "Avimex",
  "token": "<jwt — guárdalo, no se vuelve a mostrar>",
  "createdAt": "..." }
DELETE/api-tokens/:idToken / Cookie

Revoca un token. Deja de servir en API y WS de inmediato. Responde 204.

Dispositivos

Alta y gestión de gateways y sensores. El alta de MKGW4 toma el tenant del JWT y exige que el equipo esté transmitiendo por MQTT.

19 endpoints
POST/devices/mkgw4Token / Cookie

Registra un gateway MKGW4 en el tenant activo. 409 si la MAC ya existe en cualquier empresa; 424 si el equipo no está enviando datos ahora.

Cuerpo de la solicitud

{ "name": "Gateway nave 1", "mac": "FE:E4:40:3E:C9:A2" }

Respuesta

{ "id": "...", "tenantId": "tnt_avimex",
  "chipId": "fee4403ec9a2", "idOmniware": 1042,
  "name": "Gateway nave 1",
  "type": "MKGW4", "subscriptions": [], "createdAt": "..." }
GET/devices?tenantId=<id>Token / Cookie

Lista los dispositivos de un tenant. El query param tenantId es obligatorio.

Respuesta

[ { "id": "...", "name": "...", "type": "MKGW4",
    "chipId": "...", "idOmniware": 1042,
    "tenantId": "...", "online": true } ]

idOmniware es el folio del equipo: un consecutivo global que arranca en 1000, único y que nunca se reutiliza — el folio de un equipo dado de baja no vuelve a asignarse.

GET/devices/:idToken / Cookie

Detalle de un dispositivo con sus sensores.

GET/devices/:id/telemetryToken / Cookie

Histórico de telemetría (temp/hum). Filtros: from, to (ISO 8601), by=receivedAt|recordedAt (def. receivedAt: fecha de RECEPCIÓN; recordedAt: reloj del gateway), mac (un sensor). mode=raw (def.) → filas crudas con ubicación as-of; mode=bucket → serie agregada para gráficas (bucket=minute|hour|day, agg=avg|min|max). raw acepta limit (1-1000, def. 200) y skip.

Respuesta

// mode=raw  (filas crudas, orden descendente)
{ "deviceId": "...", "mode": "raw",
  "records": [
    { "recordedAt": "2026-06-26T10:15:03.000Z",
      "receivedAt": "2026-06-26T10:15:05.000Z",
      "mac": "aa:bb:cc:dd:ee:01",
      "name": "Cámara fría 1", "typeKey": "h4pro",
      "tempC": 22.4, "humPct": 48.1,
      "lat": 40.41678, "lon": -3.70379,
      "locationAt": "2026-06-26T10:14:50.000Z" } ] }

// mode=bucket&bucket=hour&agg=avg  (serie para graficar)
{ "deviceId": "...", "mode": "bucket",
  "bucket": "hour", "agg": "avg",
  "series": [
    { "t": "2026-06-26T10:00:00.000Z",
      "mac": "aa:bb:cc:dd:ee:01",
      "name": "Cámara fría 1", "typeKey": "h4pro",
      "tempC": 22.1, "humPct": 47.3, "samples": 12 } ] }

El rango from/to filtra por receivedAt (createdAt, cuándo lo registró el servidor) salvo que se pida by=recordedAt, que filtra, y en mode=bucket agrupa, por el reloj del gateway: útil cuando un equipo estuvo sin red y mandó lecturas atrasadas. En mode=bucket la serie va ascendente y agrupada por (bucket de tiempo, sensor); sin lat/lon. En mode=raw, lat/lon/locationAt van en null si no había posición previa a la lectura. name/typeKey son los del sensor en Controlware y siguen ahí aunque el sensor se haya dado de baja (en las filas de ubicación van en null).

GET/devices/:id/send-logToken / Cookie

Bitácora de envíos por sensor: cada muestra que el gateway reportó, correcta o en error (sensor sin valor). Filtros: from, to (ISO 8601, sobre el reloj del gateway), mac (un sensor), status (ok|error), limit (1-500, def. 100) y skip.

Respuesta

{ "deviceId": "...",
  "records": [
    { "source": "reading",
      "recordedAt": "2026-06-26T10:15:03.000Z",
      "receivedAt": "2026-06-26T10:15:05.000Z",
      "mac": "aa:bb:cc:dd:ee:01",
      "name": "Cámara fría 1", "typeKey": "h4pro",
      "tempC": 22.4, "humPct": 48.1,
      "battMv": 3020, "rssi": -67,
      "status": "ok", "error": null },
    { "source": "error",
      "recordedAt": "2026-06-26T10:15:03.000Z",
      "receivedAt": null,
      "mac": "ch1",
      "name": "Sonda 1", "typeKey": "pt100",
      "tempC": null, "humPct": null,
      "battMv": null, "rssi": null,
      "status": "error", "error": "rtd_open" } ] }

Orden descendente por recordedAt (reloj del gateway), común a ambas fuentes. Las filas source=error no llevan métricas: documentan que el sensor falló en ese envío (p. ej. not_found, rtd_open). receivedAt solo existe en lecturas. battMv (mV) y rssi (dBm) solo los reportan sensores BLE; en PT100 van en null. name/typeKey identifican al sensor y se conservan aunque después se haya dado de baja.

PATCH/devices/:idToken / Cookie

Renombra un dispositivo.

Cuerpo de la solicitud

{ "name": "Nuevo nombre" }
POST/devices/:id/transferToken / Cookie

Traslada un dispositivo (con todo su histórico) del tenant activo a otro tenant donde el usuario tenga membresía. 403 si no tienes acceso al tenant destino; 409 si ya existe un equipo con el mismo chip allí; 424 si un gateway Atlas rechaza o no confirma el cambio.

Cuerpo de la solicitud

{ "tenantId": "tnt_destino" }

Respuesta

{ "id": "...", "tenantId": "tnt_destino",
  "name": "...", "type": "MKGW4", "chipId": "...",
  "requiresReprovisioning": false }

Los gateways con firmware Atlas confirman el cambio por MQTT (set_tenant) y reinician para rearmar sus topics con el tenant nuevo: deben estar en línea o el traslado se aborta sin cambios. El MKGW4 se mueve directo. requiresReprovisioning=true sólo aplica a MOBILE_APP (la app toma el tenant de la sesión).

DELETE/devices/:idToken / Cookie

Da de baja un equipo del tenant activo y borra en cascada todo su histórico: sensores, lecturas, eventos, ubicaciones, alertas y reglas de alerta. Requiere rol admin u owner (403 si no); 404 si el equipo no existe en el tenant activo.

Respuesta

{ "id": "...", "chipId": "...", "idOmniware": 1042,
  "name": "...", "type": "BLE_GATEWAY",
  "removed": { "sensors": 3, "readings": 128400,
    "sensorEvents": 12, "locations": 0,
    "alerts": 7, "alertRules": 2 } }

No se le avisa al equipo: el firmware no tiene comando de baja. Un gateway Atlas que siga encendido se re-registra solo en su próximo arranque y reaparece con id nuevo y sin histórico — apágalo o reaprovisiónalo para que la baja sea definitiva. El MKGW4 no se auto-registra: su telemetría se descarta hasta darlo de alta otra vez con POST /devices/mkgw4. removed es el recuento de lo borrado. El folio idOmniware se retira con el equipo: un alta posterior toma uno nuevo, nunca el que quedó libre.

POST/devices/:id/provisioningToken / Cookie

Pone al gateway (firmware Atlas) en modo aprovisionamiento de sensores.

POST/devices/:id/logsToken / Cookie

Abre (o re-arma) el stream de logs del firmware por MQTT; con stop=true lo cierra. durationSec es opcional (def. 60, entre 5 y 3600). Sólo gateways con firmware Atlas. 424 si el gateway no responde o no pudo abrirlo (p. ej. sin memoria).

Cuerpo de la solicitud

{ "durationSec": 120 }   // cerrar: { "stop": true }

Respuesta

{ "status": "ok", "result": "started",
  "durationSec": 120, "cursor": 0 }

Cerrar es idempotente: si no había sesión corriendo responde igual con result "stopped" y wasRunning=false. cursor es la posición desde la que leer con GET /devices/:id/logs para ver sólo esta sesión. El stream NO viaja por cmd/ack: son chunks de texto plano de hasta 1 KB en el topic …/logs (QoS 0, sin retain), y el firmware cierra publicando el marcador [log_stream: fin].

GET/devices/:id/logs?cursor=0Token / Cookie

Chunks del stream publicados después de cursor (polling). Sin cursor devuelve sólo la posición actual, sin chunks, para arrancar desde el presente.

Respuesta

{ "deviceId": "...", "cursor": 42,
  "chunks": ["I (12345) atlas: ...\n"],
  "dropped": false, "ended": false }

Los logs se guardan en memoria unos minutos después del último chunk, acotados por device: dropped=true avisa que entre tu cursor y lo guardado se perdieron líneas (ráfaga o reinicio del ingestor). ended=true es el marcador de cierre del firmware: la sesión terminó.

GET/devices/:id/sensors/scan?windowMs=6000Token / Cookie

Escaneo BLE bajo demanda: el gateway escanea durante windowMs (1000–30000 ms, def. 6000) y devuelve los sensores compatibles que aún no están registrados. Sólo gateways BLE con firmware Atlas; los MKGW4 responden 400.

Respuesta

{ "count": 1,
  "found": [ { "mac": "aa:bb:cc:dd:ee:ff", "fmt": "H4Pro",
      "temp_c": 22.4, "hum_pct": 48.1,
      "batt_mv": 3010, "rssi": -63 } ] }

No persiste nada: es sólo detección (para el alta usa POST /devices/:id/sensors). count 0 con found vacío es respuesta correcta, no error. batt_mv puede faltar según el formato del advertisement.

POST/devices/:id/sensorsToken / Cookie

Asocia un sensor (por MAC) a un dispositivo. type es el modelo del sensor (opcional); el gateway debe confirmar el alta por MQTT.

Cuerpo de la solicitud

{ "mac": "AA:BB:CC:DD:EE:FF", "name": "Cámara fría", "type": "H4 Pro" }
PATCH/devices/:id/sensors/:macToken / Cookie

Renombra/retipa un sensor.

Cuerpo de la solicitud

{ "name": "Cámara fría 2", "type": "temp" }
POST/devices/:id/sensors/:mac/calibrateToken / Cookie

Ajusta el offset de calibración (°C), entre -20 y 20.

Cuerpo de la solicitud

{ "offsetC": -1.5 }
DELETE/devices/:id/sensors/:macToken / Cookie

Da de baja un sensor de un dispositivo. El sensor sale del equipo y del gateway, pero su histórico se conserva con su nombre y su tipo; volver a darlo de alta con la misma MAC reactiva el mismo sensor.

POST/devices/firmwareToken / Cookie

Publica la versión de firmware disponible para actualización de un tipo de gateway (BLE_GATEWAY o PT100_GATEWAY). Upsert global: una fila por tipo, sólo se recuerda la última versión. Requiere rol owner (403 si no).

Cuerpo de la solicitud

{ "deviceType": "BLE_GATEWAY", "version": "3.18.0" }

Respuesta

{ "deviceType": "BLE_GATEWAY", "version": "3.18.0",
  "bin": "atlas-gateway-iot-v3.18.0.bin",
  "updatedAt": "..." }

Pensado para el pipeline de release (con un token de API de un owner). Sólo registra la versión disponible — comparable contra firmwareVersion de cada equipo para saber quién tiene actualización pendiente; no avisa a la flota (para eso está el broadcast OTA).

GET/devices/firmwareToken / Cookie

Versiones de firmware disponibles por tipo de gateway (vacío si aún no se publica ningún release).

Respuesta

[ { "deviceType": "BLE_GATEWAY", "version": "3.18.0",
    "bin": "atlas-gateway-iot-v3.18.0.bin",
    "updatedAt": "..." } ]
POST/devicesPública

Auto-registro de un gateway con firmware Atlas (lo usa el propio device, sin sesión). La autorización real es el par tenantId + chipId del body.

Cuerpo de la solicitud

{ "tenantId": "<id>", "chipId": "...", "name": "...",
  "type": "BLE_GATEWAY",
  "subscriptions": [ { "topic": "...", "qos": 1 } ],
  "supportedSensorTypes": [ "H4 Pro", "CW Sensor" ],
  "sensors": [ { "ch": "ch0", "name": "Sonda 1", "type": "PT100" } ] }

supportedSensorTypes (opcional) declara los modelos BLE que el firmware sabe decodificar. sensors (opcional, sólo PT100_GATEWAY) declara los canales físicos del equipo (ch0, ch1, …) con el modelo de sonda por canal; sin él, el backend siembra ch0 y el resto se auto-registra con la primera telemetría.

Alertas

Alta y gestión de alertas sobre los equipos del tenant. Todo el módulo se acota al tenant del token: no se manda tenantId por parámetro. Los campos de contexto (site, ruleName, threshold, detectedValue) son texto libre y opcionales — documentan la alerta, no se opera con ellos.

4 endpoints
POST/alertsToken / Cookie

Da de alta una alerta manual. 404 si el device no es del tenant del token; 400 si el sensorKey no existe en ese equipo; 403 si el assigneeId no tiene acceso al tenant.

Cuerpo de la solicitud

{
  "deviceId": "<id>",
  "severity": "critical",            // critical | warning | info
  "message": "Temperatura fuera de rango",
  "sensorKey": "AA:BB:CC:DD:EE:FF",  // opcional: null = todo el equipo
  "detail": "Sensor T-0412 reporta 8.2°C...",
  "site": "Cámara fría #3 / Planta Monterrey",
  "ruleName": "Temp. máxima cámara fría",
  "threshold": "4°C",
  "detectedValue": "8.2°C",
  "triggeredAt": "2026-08-04T14:38:00.000Z",  // opcional: por defecto, ahora
  "assigneeId": "<userId>"                     // opcional
}

Respuesta

{ "id": "...", "deviceId": "...", "deviceName": "Gateway nave 1",
  "severity": "critical", "status": "active", "source": "manual",
  "message": "...", "triggeredAt": "...",
  "assignee": null, "createdBy": { "id": "...", "name": "Ana" },
  "events": [ { "type": "created", "userName": "Ana",
                "message": "Alta manual desde el panel",
                "createdAt": "..." } ] }

triggeredAt puede ser anterior al alta: documenta cuándo ocurrió el problema, mientras que createdAt es el momento del alta. source siempre es "manual" aquí; las de source "system" las genera el motor a partir de una AlertRule, y traen en deliveries por dónde salió el aviso.

GET/alerts?status=active&severity=criticalToken / Cookie

Lista las alertas del tenant, de la más reciente a la más antigua (por triggeredAt). Filtros opcionales: status, severity, deviceId, limit (1-500, def. 100) y skip.

Respuesta

[ { "id": "...", "severity": "critical", "status": "active",
    "message": "...", "deviceName": "...", "site": "...",
    "triggeredAt": "...", "resolvedAt": null, "events": [ ... ] } ]
GET/alerts/:idToken / Cookie

Detalle de una alerta con su bitácora completa. 404 si no pertenece al tenant del token.

PATCH/alerts/:idToken / Cookie

Reconoce, resuelve o reasigna una alerta. resolutionNote es opcional al resolver. 409 si ya estaba resuelta (o ya reconocida, al reconocer).

Cuerpo de la solicitud

{ "status": "resolved",              // acknowledged | resolved
  "resolutionNote": "Unidad de refrigeración reiniciada", // opcional
  "assigneeId": "<userId>" }          // opcional

Resolver es terminal: una alerta cerrada no vuelve a active — si el problema reaparece, es un alta nueva. Cada cambio deja su entrada en events (quién y cuándo); si no se envía resolutionNote, el evento queda sin mensaje. Resolver sin reconocer antes es válido y sella ambas marcas.

Salud

Sondeo del servicio.

1 endpoint
GET/healthPública

Healthcheck del API.

Tiempo real

WebSocket

Telemetría y alertas en vivo mediante Socket.IO. La conexión se asocia automáticamente con el tenant del token y sólo recibe eventos autorizados.

Conexión
Mismo origen que la web. Manda el token en auth.token del handshake (o usa la cookie de sesión en el navegador).
import { io } from "socket.io-client"

const socket = io("https://api.controlware-iot.com", {
  path: "/socket.io",
  auth: { token: "<TOKEN>" },     // Bearer del token de API o de sesión
  transports: ["websocket"],
})

socket.on("connect", () => console.log("conectado", socket.id))
socket.on("connect_error", (e) => console.error(e.message)) // "unauthorized"
socket.on("telemetry", (msg) => console.log(msg))
socket.on("alert", (msg) => console.log(msg))

Sin token válido la conexión se rechaza en el handshake (connect_error: unauthorized) — nunca llega a connect. Si revocas el token, deja de conectar.

Evento telemetry
Se emite por cada lectura. El campo id identifica al gateway; filtra por él en el cliente. Hay tres formas según el origen.

Gateway BLE (firmware Atlas) — enriquecido con nombres

{
  "name": "Gateway nave 1",
  "id": "<deviceId>",
  "sensors": [
    { "name": "Cámara fría", "mac": "aa:bb:cc:dd:ee:ff", "t": 4.2 }
  ]
}

Gateway MKGW4 — lecturas de sensores

{
  "id": "<deviceId>",
  "name": "Gateway nave 1",
  "sensors": [ { "mac": "...", "t": 4.2, "rssi": -71 } ]
}

Gateway MKGW4 — ubicación (GPS o LBS)

{
  "id": "<deviceId>",
  "name": "Gateway nave 1",
  "location": { "lat": 19.43, "lon": -99.13 },   // o { "error": "lbs" }
  "receivedAt": "2026-06-26T21:43:22.748Z"
}

Sólo recibes la telemetría de los gateways registrados en tu tenant. Un MKGW4 que no esté dado de alta se descarta (no se enruta).

Evento alert
Se emite cuando una regla dispara y tiene el canal «panel en vivo» activo. Es el mismo cuerpo que recibe un webhook genérico.
{
  "id": "<alertId>",
  "tenantId": "<tenantId>",
  "deviceId": "<deviceId>",
  "deviceName": "Gateway nave 1",
  "sensorKey": "aa:bb:cc:dd:ee:ff",
  "severity": "critical",              // critical | warning | info
  "message": "Temperatura > 4 °C — Cámara fría",
  "detail": "Temperatura de Cámara fría (aa:bb:cc:dd:ee:ff) en > 4 °C...",
  "site": "Planta Monterrey",
  "ruleId": "<ruleId>",
  "ruleName": "Temperatura > 4 °C — Cámara fría",
  "metric": "temp_c",
  "threshold": "> 4 °C",
  "detectedValue": "8.2 °C",
  "triggeredAt": "2026-08-20T21:43:22.748Z"
}

La alerta ya está guardada cuando esto se emite: el evento es el aviso, no la fuente de verdad. Si tu cliente estaba desconectado, recupérala con GET /alerts.

Si la regla tiene recordatorios (renotifyMinutes), la misma alerta se reemite cada N minutos mientras siga abierta y el problema siga ahí, con reminder: true y el mismo id — el webhook lo marca además en la cabecera X-Atlas-Event: alert.reminder. Deduplica por id si no quieres tratarlas como alertas distintas.

Evento ping → pong
Sondeo de ida y vuelta opcional.
socket.emit("ping", { hola: 1 }, (res) => {
  // res === { event: "pong", data: { hola: 1 } }
})