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"
- Permisos:
lectura(consultar) yempleados:escritura(además, crear, modificar y dar de baja empleados). - Límite: 600 pedidos por minuto por clave. Si se supera, la respuesta es
429con el encabezadoretry-after. - Errores: código HTTP y cuerpo
{"error": "mensaje"}. 401 clave inválida, 403 sin permiso, 404 no existe, 409 conflicto, 422 datos inválidos. - Las plantillas de huella y las claves de los empleados nunca salen por la API.
Endpoints
/api/v1/branchespermiso: lecturaSucursales de la empresa.
{ "data": [ { "id": "…", "name": "Casa central", "address": "…", "active": true } ] }/api/v1/employeespermiso: lecturaFiltros 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
}/api/v1/employeespermiso: empleados:escrituraAlta 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.
/api/v1/employees/{external_ref}permiso: lecturaUn empleado. Para buscar por legajo: /api/v1/employees/118?by=legajo.
/api/v1/employees/{external_ref}permiso: empleados:escrituraBaja: 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.
/api/v1/punchespermiso: lecturaFichadas 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.
/api/v1/attendancepermiso: lecturaJornadas 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:
punch.created: entró una fichada (de cualquier origen).device.offline/device.online: un reloj dejó de conectarse más de 30 minutos / volvió.employee.updated: alta, cambio o baja de un empleado.
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.
