Ir al contenido

Molinetes

Cómo conectar un molinete, una puerta o su placa controladora al control de acceso de un gimnasio. El equipo consulta a Kaptativa cada vez que lee un QR, un DNI o una tarjeta, y abre solo si el socio puede pasar.

Cómo funciona

  1. En el panel, el gimnasio abre Socios > Accesos > Molinetes y crea un molinete por cada punto de paso, con su sede y lo que lee (QR del carnet, DNI, tarjeta o llavero).
  2. Al crearlo se muestran el identificador (cuenta.controlador), la clave secreta y la dirección de la API. La clave se ve una sola vez; si se pierde, Clave nueva la reemplaza y la anterior deja de servir en el momento.
  3. El equipo (la placa, un puente local o un script en una PC de la recepción) manda cada lectura a validar. La respuesta dice si abre y por cuántos segundos. Decide con el mismo criterio que la recepción: el estado del socio, la cuota vencida, el plan (días, horario, ingresos por semana y sedes), el apto médico y el antirretorno.
  4. Cada intento queda en el Registro de Accesos con el nombre del molinete, pase o no.

Dirección base: https://panel.kaptativa.app/api/molinetes/v1. Todo va por HTTPS, con el cuerpo en JSON.

La firma de cada pedido

Cada pedido lleva tres encabezados. Sin ellos, o con la firma mal, la respuesta es 401 y no se registra nada.

Encabezados
EncabezadoQué lleva
x-kaptativa-controladorEl identificador del molinete, tal como lo muestra el panel.
x-kaptativa-fechaLa hora del equipo en segundos Unix. Se acepta hasta 5 minutos de diferencia con el servidor: el equipo tiene que tener la hora por NTP.
x-kaptativa-firmaHMAC-SHA256 en hexadecimal, con la clave secreta, del texto que se arma abajo.

El texto que se firma son cuatro renglones separados por un salto de línea (\n): la fecha (igual que en el encabezado), el método en mayúsculas, la ruta (por ejemplo /api/molinetes/v1/validar, sin el dominio ni la consulta) y el SHA-256 en hexadecimal del cuerpo exacto que se manda (el del texto vacío en un GET).

Node.js
import { createHash, createHmac } from 'node:crypto';

const IDENTIFICADOR = process.env.KAPTATIVA_CONTROLADOR; // cuenta.controlador, del panel
const CLAVE = process.env.KAPTATIVA_CLAVE;               // se muestra una sola vez
const BASE = 'https://panel.kaptativa.com';

async function pedir(metodo, accion, cuerpo) {
  const ruta = `/api/molinetes/v1/${accion}`;
  const texto = cuerpo ? JSON.stringify(cuerpo) : '';
  const fecha = String(Math.floor(Date.now() / 1000));
  const firmado = [fecha, metodo, ruta, createHash('sha256').update(texto).digest('hex')].join('\n');
  const firma = createHmac('sha256', CLAVE).update(firmado).digest('hex');
  const r = await fetch(BASE + ruta, {
    method: metodo,
    headers: { 'content-type': 'application/json', 'x-kaptativa-controlador': IDENTIFICADOR, 'x-kaptativa-fecha': fecha, 'x-kaptativa-firma': firma },
    body: texto || undefined,
    signal: AbortSignal.timeout(3000)
  });
  return r.json();
}

// Cuando el lector entrega un DNI, una tarjeta o el texto del QR:
const r = await pedir('POST', 'validar', { lecturaId: crypto.randomUUID(), tipo: 'qr', valor: textoDelQr });
if (r.abrir) pulsarRele(r.segundos);
mostrarEnVisor(r.mensaje);
Python
import hashlib, hmac, json, time, uuid, requests

def pedir(metodo, accion, cuerpo=None):
    ruta = f"/api/molinetes/v1/{accion}"
    texto = json.dumps(cuerpo, separators=(",", ":")) if cuerpo is not None else ""
    fecha = str(int(time.time()))
    firmado = "\n".join([fecha, metodo, ruta, hashlib.sha256(texto.encode()).hexdigest()])
    firma = hmac.new(CLAVE.encode(), firmado.encode(), hashlib.sha256).hexdigest()
    h = {"content-type": "application/json", "x-kaptativa-controlador": IDENTIFICADOR, "x-kaptativa-fecha": fecha, "x-kaptativa-firma": firma}
    return requests.request(metodo, BASE + ruta, data=texto or None, headers=h, timeout=3).json()

r = pedir("POST", "validar", {"lecturaId": str(uuid.uuid4()), "tipo": "dni", "valor": "30123456"})
La clave no sale del equipo

Guardala en la placa o el puente, nunca en una página web ni en una app del celular. Si el equipo se pierde o se cambia, tocá Clave nueva o Dar de baja en el panel.

Validar una lectura

POST/api/molinetes/v1/validar
Cuerpo de validar
CampoQué lleva
lecturaIdUn id propio de la lectura, de 1 a 64 letras, números, punto, guion o dos puntos. Si la red falla y se reintenta con el mismo id, la respuesta es la misma y no se registra dos veces.
tipoqr, dni o tarjeta. Si el molinete no tiene habilitado ese tipo, no abre.
valorEl texto del QR tal cual (empieza con SC1. y vence a los 60 segundos), el DNI (solo dígitos) o el código de la tarjeta (sin separadores; se aceptan mayúsculas o minúsculas). Hasta 300 caracteres.
Respuesta de validar
{
  "abrir": true,
  "resultado": "habilitado",
  "segundos": 3,
  "ingresoId": "0193a1f0-7c2e-7d4a-9b1c-2d3e4f5a6b7c",
  "repetida": false,
  "mensaje": "Hola, Lucía. Adelante."
}
  • abrir: pulsar el relé o liberar el molinete por segundos.
  • resultado: habilitado, advertencia (pasa, pero la recepción tiene un aviso), denegado o pausa.
  • mensaje: un texto corto para el visor. Nunca dice el motivo de un rechazo (una deuda o el apto): eso lo ve la recepción en el Registro.
  • Después de 8 rechazos en 2 minutos el molinete responde pausa un rato, para frenar a quien prueba DNI o tarjetas al azar.
  • Conviene un tiempo de espera corto (2 o 3 segundos). Si no hay respuesta, usar la lista sin conexión.

Latido y apertura manual

POST/api/molinetes/v1/latido

Mandalo cada 5 a 10 segundos con {"version": "1.2.0"} (la versión del firmware o del puente, opcional) y, al volver de un corte, sinConexionDesde con la hora en ISO 8601. El panel muestra el molinete como conectado si hubo contacto en los últimos 2 minutos.

La respuesta trae las aperturas manuales que pidió la recepción (con el permiso Forzar ingresos y un motivo). Cada una se entrega una sola vez y vence a los 60 segundos si el molinete no la recibió.

Respuesta del latido
{
  "hora": "2026-11-16T15:00:00.000Z",
  "comandos": [
    {
      "id": "0193a1f0-8d3f-7e5b-8c2d-3e4f5a6b7c8d",
      "tipo": "abrir",
      "segundos": 3
    }
  ]
}

Sin conexión: la lista firmada

GET/api/molinetes/v1/lista

Devuelve la lista de credenciales que hoy pasarían en esa sede, para decidir solo si se corta internet. Vence a los 60 minutos (con ?minutos= se pide entre 5 y 240). Pedila de nuevo cada 10 a 15 minutos y descartala al vencer.

Respuesta de la lista
{
  "contenido": "{\"version\":1,...}",
  "firma": "hmac-sha256 en hexadecimal"
}

firma es el HMAC-SHA256 de contenido (el texto exacto, antes de leerlo como JSON) con la clave del molinete. Verificala antes de usar la lista. Adentro de contenido:

contenido
{
  "version": 1,
  "controlador": "cuenta.controlador",
  "cuentaId": "0193…",
  "sucursalId": "0193…",
  "zona": "America/Argentina/Buenos_Aires",
  "emitida": "2026-11-16T15:00:00.000Z",
  "vence": "2026-11-16T16:00:00.000Z",
  "segundos": 3,
  "lecturas": [
    "qr",
    "dni",
    "tarjeta"
  ],
  "hash": "sha256(tipo:cuentaId:valor)",
  "entradas": [
    {
      "h": "9f2c…",
      "t": "dni",
      "dias": [
        1,
        3,
        5
      ],
      "desde": "07:00",
      "hasta": "22:00"
    },
    {
      "h": "41ab…",
      "t": "tarjeta",
      "dias": null,
      "desde": null,
      "hasta": null
    }
  ]
}
  • No lleva nombres, DNI, números de socio ni teléfonos. Cada entrada es el SHA-256 en hexadecimal de tipo:cuentaId:valor: para un DNI, el valor son los dígitos sin ceros a la izquierda; para una tarjeta, el código sin separadores y en mayúsculas; para el QR, el segundo tramo del texto (el id del socio, entre el primer y el segundo punto).
  • dias (0 domingo a 6 sábado), desde y hasta son las reglas del plan en la hora local de zona; null no limita.
  • Sin conexión no se controlan los ingresos por semana, el antirretorno ni la firma del QR (el equipo no tiene la clave del servidor): solo que el QR no esté vencido según su tercer tramo, el vencimiento en segundos Unix. Es menos seguro que en línea: si el gimnasio lo prefiere, el equipo puede aceptar sin conexión solo DNI y tarjeta y dejar el QR para cuando vuelva internet.
  • Un hash de DNI se puede adivinar probando números: tratá la lista como confidencial, igual que la clave.

Subir lo que pasó sin conexión

POST/api/molinetes/v1/sincronizar

Al volver la conexión, mandá lo que decidió el equipo solo, hasta 500 lecturas por pedido. Quedan en el Registro marcadas como sin conexión, con la hora del equipo y si abrió o no. Repetir un lecturaId no lo registra dos veces; lo de más de 7 días o con hora futura se rechaza.

Cuerpo de sincronizar
{
  "lecturas": [
    {
      "lecturaId": "zk-000123",
      "tipo": "tarjeta",
      "valor": "04A31F9C",
      "instante": "2026-11-16T14:10:22-03:00",
      "abrio": true
    }
  ]
}
Respuesta de sincronizar
{
  "registradas": 1,
  "repetidas": 0,
  "rechazadas": []
}

Errores

Códigos
EstadoCódigoCuándo
401identificador_invalidoFalta el identificador o no tiene la forma cuenta.controlador.
401fecha_invalidaFalta la fecha o el reloj del equipo está corrido.
401firma_invalidaLa firma no coincide: revisá la clave, la ruta y que el cuerpo firmado sea el mismo que se manda.
401controlador_desconocidoEl molinete no existe, se dio de baja o la cuenta no tiene Accesos activo.
400datos_invalidosFalta un campo o tiene otro formato.
404no_existeLa dirección o el método no son los de esta página.

Los errores llegan como {"error": {"codigo": "...", "mensaje": "..."}}.

Equipos comunes

Placa con relé que hace pedidos HTTP

Las controladoras programables (ESP32 con lector Wiegand o QR por serie, placas con firmware propio) pueden firmar y llamar a validar directamente y pulsar el relé con la respuesta. Hace falta HTTPS con verificación del certificado y la hora por NTP.

ZKTeco

Los molinetes y lectores de ZKTeco suelen trabajar con su propio software (ZKBio, el protocolo PUSH o la SDK). Lo más simple es un puente local: un programa en una PC o una Raspberry Pi de la red del gimnasio que recibe la lectura del equipo, consulta a validar y le ordena abrir al equipo por la SDK o por el relé auxiliar. El equipo queda en modo "verificación por servidor" (o en modo tarjeta sin usuarios cargados) para que no decida solo.

Hikvision

Los terminales de control de acceso de Hikvision hablan ISAPI. El puente local se suscribe a los eventos de lectura (alertStream o el modo de verificación remota), consulta a validar y abre la puerta con la orden de ISAPI de control remoto. Igual que con ZKTeco, la clave de Kaptativa queda en el puente, no en el terminal.

Lector que tipea (USB) en una PC

Un lector de QR o de tarjetas que se comporta como teclado, en una PC con un relé USB: un script lee el renglón, consulta a validar y acciona el relé. Para no tener nada que instalar, la tablet de autoingreso (Socios > Accesos > Tablets) cubre el caso sin relé.

Lo que define el instalador

Kaptativa no maneja el hardware: el instalador elige el equipo, el puente si hace falta, cómo se acciona el relé, qué hace el molinete ante un corte de luz (abierto o cerrado, según las normas de evacuación) y cada cuánto se refresca la lista. Antes de dejarlo andando, probalo con un socio al día, uno con la cuota vencida, uno de otra sede y el corte de internet.