HuellaOk

API para integraciones

La API de HuellaOk permite leer empleados, fichadas y jornadas, y dar de alta o de baja empleados desde otro sistema (sueldos, ERP, GeriControl o un desarrollo propio). Todas las respuestas son JSON. Las fechas sin hora son YYYY-MM-DD en la zona horaria de la empresa; los instantes van en ISO 8601 (UTC).

Autenticación

Un administrador crea la clave en Configuración → Integraciones. La clave empieza con hok_, se muestra una sola vez y se puede revocar. Se manda en cada pedido:

curl https://huellaok.com/api/v1/branches \
  -H "Authorization: Bearer hok_TU_CLAVE"

Endpoints

GET/api/v1/branchespermiso: lectura

Sucursales de la empresa.

{ "data": [ { "id": "…", "name": "Casa central", "address": "…", "active": true } ] }
GET/api/v1/employeespermiso: lectura

Filtros opcionales: active=true|false, branch_id, q (apellido, nombre, legajo o DNI). Paginado con limit (máx. 1000) y after = el next_after de la página anterior.

{
  "data": [ { "id": "…", "external_ref": "GC-118", "legajo": "118", "first_name": "Ana", "last_name": "Gómez",
              "cuil": "27-…", "branch_id": "…", "biometric_pin": "118", "active": true, … } ],
  "next_after": null
}
POST/api/v1/employeespermiso: empleados:escritura

Alta o actualización. Si mandás external_ref se busca por ese campo; si no, por legajo. Obligatorios: legajo, first_name, last_name. El biometric_pin (número con el que la persona existe en el reloj) es por defecto el legajo y tiene que ser numérico de hasta 9 dígitos. El CUIL se valida.

curl -X POST https://huellaok.com/api/v1/employees \
  -H "Authorization: Bearer hok_TU_CLAVE" -H "Content-Type: application/json" \
  -d '{"external_ref":"GC-118","legajo":"118","first_name":"Ana","last_name":"Gómez",
       "cuil":"27-23456789-1","branch_id":"…"}'

Responde 201 si lo creó o 200 si lo actualizó, con el empleado en data.

GET/api/v1/employees/{external_ref}permiso: lectura

Un empleado. Para buscar por legajo: /api/v1/employees/118?by=legajo.

DELETE/api/v1/employees/{external_ref}permiso: empleados:escritura

Baja: lo marca inactivo con fecha de egreso, lo quita de los relojes y borra sus huellas. El historial de fichadas se conserva. También acepta ?by=legajo.

GET/api/v1/punchespermiso: lectura

Fichadas crudas entre from y to (por defecto, hoy). Filtros: branch_id, employee_id. Paginado por cursor: limit (máx. 1000) y cursor = el next_cursor anterior. Vienen ordenadas por hora.

curl "https://huellaok.com/api/v1/punches?from=2026-10-01&to=2026-10-31&limit=1000" \
  -H "Authorization: Bearer hok_TU_CLAVE"

{ "data": [ { "id": "…", "legajo": "118", "external_ref": "GC-118", "pin": "118",
              "punched_at": "2026-10-07T11:01:23.000Z", "punched_local": "2026-10-07 08:01:23",
              "status_code": 0, "verify_method": "huella", "source": "reloj", … } ],
  "next_cursor": "MjAyNi0xMC0wN1Qx…" }

source: reloj, hikvision, conector, archivo, web, kiosco. status_code: el tipo que marcó la persona en el reloj (0 entrada, 1 salida, 2/3 descanso, 4/5 extras, 255 sin indicar); los cálculos no dependen de él.

GET/api/v1/attendancepermiso: lectura

Jornadas calculadas (ya con horarios, feriados, licencias y correcciones) entre from y to (obligatorios, máx. 62 días), por empleado, con totales del período. Filtros: branch_id, employee_id. Todos los tiempos en minutos.

{ "from": "2026-10-01", "to": "2026-10-31",
  "data": [ {
    "employee": { "id": "…", "legajo": "118", "external_ref": "GC-118", … },
    "totals": { "days_worked": 21, "regular_min": 10080, "overtime50_min": 240, "overtime100_min": 120,
                "night_min": 0, "holiday_worked_min": 0, "late_min": 35, "late_count": 3,
                "unjustified_absences": 1, "leaves": { "ENF": 2 } },
    "days": [ { "date": "2026-10-01", "status": "presente", "first_in": "…", "last_out": "…",
                "worked_min": 482, "late_min": 0, "overtime50_min": 0, … } ]
  } ] }

status de cada día: presente, en_curso, incompleto, ausente, licencia, feriado, franco, pendiente.

Webhooks

En Configuración → Integraciones se cargan URLs https:// públicas que reciben un POST con JSON por cada evento:

POST https://tu-sistema.com/huellaok
x-huellaok-event: punch.created
x-huellaok-delivery: 1842
x-huellaok-timestamp: 1791370883
x-huellaok-signature: sha256=5d41402abc4b2a76b9719d911017c592…

{ "event": "punch.created", "created_at": "2026-10-07T11:01:24.120Z",
  "data": { "id": "…", "pin": "118", "legajo": "118", "external_ref": "GC-118",
            "punched_at": "2026-10-07T11:01:23.000Z", "verify_method": "huella", "source": "reloj", … } }

Verificar la firma

La firma es un HMAC-SHA256, en hexadecimal, de timestamp + "." + cuerpo con el secreto del webhook (se muestra una sola vez al crearlo). Compará en tiempo constante y rechazá timestamps de más de 5 minutos.

import crypto from "node:crypto";

function verify(req, rawBody, secret) {
  const ts = req.headers["x-huellaok-timestamp"];
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  const got = req.headers["x-huellaok-signature"] ?? "";
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

Reintentos

Respondé con un código 2xx en menos de 10 segundos. Si no, se reintenta a los 1, 5, 30 minutos, 2, 6, 12 y 24 horas; después se marca como fallido. Las redirecciones no se siguen. Un mismo evento puede llegar más de una vez: usá el id de la fichada para no duplicar.