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.
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.
https://api.controlware-iot.com/apiPrimera 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 tokensCookie 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.
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.
/auth/registerPúblicaCrea 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"
}/auth/loginPúblicaAutentica por email/contraseña. Misma respuesta que register.
Cuerpo de la solicitud
{ "email": "[email protected]", "password": "********" }/auth/refreshPúblicaRota el refresh token (revoca el actual y emite uno nuevo) y devuelve un nuevo accessToken.
Cuerpo de la solicitud
{ "refreshToken": "<opaco>" }/auth/logoutPúblicaCierra sesión: limpia la cookie y revoca el refresh token si se envía. Responde 204.
Cuerpo de la solicitud
{ "refreshToken": "<opaco>" } // opcional/auth/meToken / CookieDevuelve 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.
/membershipsToken / CookieLista los tenants a los que el usuario tiene acceso.
Respuesta
[ { "role": "owner",
"tenant": { "id": "...", "name": "Avimex", "slug": "avimex" } } ]/tenants/switchToken / CookieCambia 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.
/api-tokensToken / CookieEmpresa, 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": "..." } ]
}/api-tokensToken / CookieGenera 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": "..." }/api-tokens/:idToken / CookieRevoca 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.
/devices/mkgw4Token / CookieRegistra 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": "..." }/devices?tenantId=<id>Token / CookieLista 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.
/devices/:idToken / CookieDetalle de un dispositivo con sus sensores.
/devices/:id/telemetryToken / CookieHistó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).
/devices/:id/send-logToken / CookieBitá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.
/devices/:idToken / CookieRenombra un dispositivo.
Cuerpo de la solicitud
{ "name": "Nuevo nombre" }/devices/:id/transferToken / CookieTraslada 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).
/devices/:idToken / CookieDa 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.
/devices/:id/provisioningToken / CookiePone al gateway (firmware Atlas) en modo aprovisionamiento de sensores.
/devices/:id/logsToken / CookieAbre (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].
/devices/:id/logs?cursor=0Token / CookieChunks 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ó.
/devices/:id/sensors/scan?windowMs=6000Token / CookieEscaneo 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.
/devices/:id/sensorsToken / CookieAsocia 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" }/devices/:id/sensors/:macToken / CookieRenombra/retipa un sensor.
Cuerpo de la solicitud
{ "name": "Cámara fría 2", "type": "temp" }/devices/:id/sensors/:mac/calibrateToken / CookieAjusta el offset de calibración (°C), entre -20 y 20.
Cuerpo de la solicitud
{ "offsetC": -1.5 }/devices/:id/sensors/:macToken / CookieDa 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.
/devices/firmwareToken / CookiePublica 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).
/devices/firmwareToken / CookieVersiones 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": "..." } ]/devicesPúblicaAuto-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.
/alertsToken / CookieDa 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.
/alerts?status=active&severity=criticalToken / CookieLista 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": [ ... ] } ]/alerts/:idToken / CookieDetalle de una alerta con su bitácora completa. 404 si no pertenece al tenant del token.
/alerts/:idToken / CookieReconoce, 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>" } // opcionalResolver 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.
/healthPúblicaHealthcheck 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.
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.
telemetryid 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).
alert{
"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.
ping → pongsocket.emit("ping", { hola: 1 }, (res) => {
// res === { event: "pong", data: { hola: 1 } }
})