Control Horario·Legal
Para desarrolladores · API REST v1

API REST

Conecta tus fichajes con tu ERP, tu cuadro de mando o tus propias herramientas. Una API JSON para consultar empleados, centros, fichajes, presencia, horarios previstos, correcciones y proyectos, con tu propio código de proyecto en cada apunte y las horas imputadas frente a las previstas — y, si lo necesitas, para registrar fichajes desde tu propio portal. Disponible en el plan Pro.

Toda clave nace de solo lectura. El registro de fichajes y la resolución de correcciones exigen un permiso de escritura que concedemos caso por caso: escribir en el registro es escribir en la cadena de hashes que lo hace inalterable, y eso no se abre por defecto.

§ 01 · Autenticación

Una API key por empresa.

Cada empresa genera su clave desde el panel, en Integraciones (/integraciones), con sesión de RRHH o administración. El token se muestra una sola vez: en nuestra base de datos solo queda su hash SHA-256 y un prefijo legible para que puedas identificar la clave en el listado. Puedes crear y revocar claves cuando quieras; al revocarlas dejan de funcionar al instante. Hasta 10 claves activas por empresa. Toda petición se filtra automáticamente por tu empresa — solo ves tus propios datos.

Envía la clave en la cabecera X-Api-Key en cada petición:

curl https://controlhorariolegal.com/api/v1/ping \
  -H "X-Api-Key: chl_tu_clave_aqui"

También se acepta Authorization: Bearer chl_tu_clave_aqui. Las peticiones van siempre por HTTPS y las respuestas son JSON con codificación UTF-8.


§ 02 · Endpoints de consulta

Base: https://controlhorariolegal.com/api/v1

Todos los endpoints de esta sección funcionan con una clave de solo lectura.

GET /ping

Comprueba que tu credencial es válida y devuelve los datos básicos de tu empresa y plan.

{
  "ok": true,
  "empresa": "Mi Empresa S.L.",
  "cif": "B12345678",
  "plan": "pro",
  "fecha": "2026-06-18T13:30:00+02:00"
}

GET /empleados

Lista los empleados activos de tu empresa, ordenados por apellidos.

{
  "ok": true,
  "total": 2,
  "empleados": [
    {
      "id": 14,
      "nombre": "Ana",
      "apellidos": "García López",
      "dni": "12345678Z",
      "email": "ana@empresa.es",
      "rol": "empleado",
      "id_centro": 3,
      "requiere_fichaje": true,
      "horas_semana": 40
    }
  ]
}

GET /centros

Lista los centros de trabajo de tu empresa.

{
  "ok": true,
  "total": 1,
  "centros": [
    {
      "id": 3,
      "nombre": "Oficina Central",
      "direccion": "Calle Mayor 1",
      "poblacion": "Madrid",
      "codigo_postal": "28013",
      "fichaje_activo": true
    }
  ]
}

GET /fichajes

Lista fichajes en un rango de fechas. Cada fila es un apunte individual (entrada, salida, pausa_inicio, pausa_fin), no un par cerrado: vienen ordenados cronológicamente para que puedas emparejarlos. Parámetros opcionales:

  • desde, hasta — formato YYYY-MM-DD. Por defecto, los últimos 30 días. Máximo 366 días por consulta.
  • dni — filtra por un empleado (acepta DNI/NIE o email).
  • id_empleado — alternativa al DNI, con el id devuelto por /empleados.
  • id_centro — filtra por un centro.
  • referencia — filtra por tu código propio de proyecto u obra. Coincidencia exacta.
  • geo — 1 para incluir la geolocalización de cada fichaje. Fuera por defecto.
curl "https://controlhorariolegal.com/api/v1/fichajes?desde=2026-06-01&hasta=2026-06-18" \
  -H "X-Api-Key: chl_tu_clave_aqui"
{
  "ok": true,
  "desde": "2026-06-01",
  "hasta": "2026-06-18",
  "referencia": null,
  "total": 1,
  "truncado": false,
  "fichajes": [
    {
      "id": 9021,
      "empleado": "Ana García López",
      "dni": "12345678Z",
      "centro": "Oficina Central",
      "tipo": "entrada",
      "ts": "2026-06-18 07:02:11.482",
      "fecha": "2026-06-18",
      "hora": "09:02",
      "metodo": "kiosko_pin",
      "referencia": "PRY-2026/014",
      "hash": "a3f1c9...",
      "proyecto": { "id": 7, "codigo": "PRY-2026/014", "nombre": "Reforma Nave B" }
    }
  ]
}

proyecto vale null si ese apunte no está imputado a ningún proyecto del maestro. Ver § 04 · Proyectos.

Tu propia referencia de proyecto

Cada fichaje puede llevar una referencia libre de hasta 64 caracteres: tu código de proyecto, de obra o de expediente. Es un dato opaco — no lo interpretamos, solo lo guardamos y te lo devolvemos — y se escribe al registrar el fichaje (POST /fichajes). Se admiten letras, números, espacios y los signos . _ - / # : ( ).

La referencia no entra en el hash del registro: el hash cubre el hecho jurídico —quién, cuándo y qué tipo de apunte—, y esa fórmula no cambia. Su inmutabilidad la garantiza el propio motor de base de datos, que no admite modificar ni borrar un fichaje una vez escrito.

Si esa referencia coincide con el codigo de un proyecto de tu maestro, el fichaje queda imputado automáticamente a ese proyecto y empieza a sumar en /proyectos/horas. Si no coincide con ninguno, la referencia se guarda igual: no rechazamos un fichaje porque el proyecto no esté dado de alta todavía.

Geolocalización

Con ?geo=1 cada fichaje incluye un objeto geo con las coordenadas registradas, la precisión del GPS en metros, la distancia al centro y si quedó dentro del perímetro configurado. Los campos valen null si ese fichaje se registró sin ubicación.

"geo": {
  "lat": 40.4321,
  "lng": -3.8123,
  "precision": 18,
  "distancia": 24,
  "valida": true
}

Está fuera por defecto a propósito: la ubicación de una persona no debería viajar a un sistema externo sin que alguien lo pida expresamente. Si la exportas a otra herramienta, recuerda que sigue siendo un dato personal — refléjalo en tu registro de actividades de tratamiento y en la información que das a tu plantilla.

Una consulta devuelve como máximo 10.000 fichajes. Si se alcanza ese tope, truncado vale true; acota el rango de fechas para obtener el resto.

GET /horas

Horas trabajadas por empleado en un periodo, ya sumadas. Es el atajo cuando lo que necesitas es el total y no te interesa emparejar apuntes tú mismo. Usa exactamente el mismo cálculo que la columna de horas del export para nómina, así que la integración y el CSV que abre la gestoría no pueden dar cifras distintas. Solo cuenta jornadas cerradas: una entrada sin salida es un dato incompleto.

  • desde, hasta — formato YYYY-MM-DD. Por defecto, el mes en curso. Máximo 366 días por consulta.
  • dni — filtra por un empleado (acepta DNI/NIE o email).
  • id_empleado — alternativa al DNI.
  • id_centro — filtra por un centro.
curl "https://controlhorariolegal.com/api/v1/horas?desde=2026-08-01&hasta=2026-08-31" \
  -H "X-Api-Key: chl_tu_clave"

{
  "ok": true,
  "desde": "2026-08-01",
  "hasta": "2026-08-31",
  "total": 1,
  "empleados": [
    {
      "id_empleado": 42,
      "nombre": "Ana",
      "apellidos": "García López",
      "dni": "12345678Z",
      "sociedad": "Ejemplo S.L.",
      "horas_semana": 35,
      "dias": 18,
      "segundos": 453600,
      "horas": 126.0,
      "hhmm": "126:00"
    }
  ]
}

GET /estado

Situación actual de un empleado: si está dentro, fuera o en pausa, qué acciones puede hacer ahora, lo trabajado hoy y si hay una ausencia aprobada que cubra el día. Pensado para pintar el botón de fichar en tu propio portal sin duplicar nuestras reglas de secuencia. Identifica al empleado con dni, email o id_empleado.

{
  "ok": true,
  "empleado": { "id": 14, "nombre": "Ana García López", "rol": "empleado",
                "id_centro": 3, "centro": "Oficina Central", "requiere_fichaje": true },
  "fecha": "2026-06-18",
  "ahora": "13:30",
  "solo_kiosko": false,
  "estado": "dentro",
  "acciones": ["pausa_inicio", "salida"],
  "avisar": false,
  "motivo": "jornada_abierta",
  "jornada": { "hora_entrada": "09:02", "horas_abierta": 4.5, "olvidada": false },
  "ausencia": null,
  "hoy": {
    "trabajado": "04:28",
    "en_pausa": false,
    "fichajes": [ { "tipo": "entrada", "hora": "09:02", "metodo": "kiosko_pin" } ]
  }
}

avisar indica si procede recordarle que fiche, y motivo explica por qué sí o por qué no (sin_entrada_hoy, ausencia, jornada_abierta, solo_kiosko…).

GET /presencia

Quién está dentro ahora y desde qué hora, para toda la plantilla obligada a fichar, en una sola llamada. Parámetro opcional id_centro. Es el equivalente al panel de un responsable: con /estado harían falta tantas llamadas como empleados.

{
  "ok": true,
  "total": 12,
  "dentro": 7,
  "empleados": [
    {
      "id": 14,
      "nombre": "Ana García López",
      "rol": "empleado",
      "id_centro": 3,
      "centro": "Oficina Central",
      "estado": "dentro",
      "hora_entrada": "09:02",
      "trabajado": "04:28",
      "fichajes_hoy": [ { "tipo": "entrada", "hora": "09:02" } ]
    }
  ]
}

GET /horario

Turnos asignados a un empleado en un rango: no cuándo fichó, sino cuándo le toca. Identifica al empleado con dni, email o id_empleado. desde y hasta son opcionales (por defecto, la semana en curso); máximo 62 días por consulta. Los días se devuelven indexados por fecha, y un mismo día puede tener varios tramos (jornada partida).

{
  "ok": true,
  "empleado": { "id": 14, "nombre": "Ana García López" },
  "desde": "2026-06-15",
  "hasta": "2026-06-21",
  "dias": {
    "2026-06-15": [
      { "fecha": "2026-06-15", "id_turno": 2, "nombre": "Mañana",
        "hora_inicio": "09:00", "hora_fin": "17:00", "descanso_min": 30,
        "nota": null, "color": "#2f6f4f", "a_medida": 0 }
    ]
  }
}
§ 03 · Correcciones

Olvidos: se piden, se aprueban, no se editan.

Un fichaje nunca se modifica ni se borra. Cuando alguien olvida fichar, se registra una solicitud de corrección que alguien con permiso aprueba o rechaza; si se aprueba, nace un fichaje nuevo con su hora, su motivo y su rastro de quién lo autorizó. La API expone ese circuito completo.

GET /correcciones/listar

Solicitudes de un empleado (dni, email o id_empleado) con su estado y la respuesta de RRHH. Últimas 100.

{
  "ok": true,
  "total": 1,
  "correcciones": [
    {
      "id": 341,
      "tipo": "salida",
      "fecha": "2026-06-17",
      "hora": "18:00",
      "motivo": "Olvidé fichar la salida",
      "estado": "aprobado",
      "respuesta": "Confirmado con el encargado"
    }
  ]
}

GET /correcciones/pendientes

Cola de solicitudes pendientes de revisión en toda la empresa. Parámetro opcional id_centro. Últimas 200.

{
  "ok": true,
  "total": 1,
  "correcciones": [
    {
      "id": 341,
      "empleado": "Ana García López",
      "id_centro": 3,
      "centro": "Oficina Central",
      "tipo": "salida",
      "fecha": "2026-06-17",
      "hora": "18:00",
      "motivo": "Olvidé fichar la salida",
      "solicitada": "2026-06-18 08:14:02"
    }
  ]
}

POST /correcciones · requiere escritura

Crea una solicitud de corrección. No escribe en el registro: lo que llega es una petición, no un apunte. Las horas se envían en hora local del centro y se convierten a UTC al guardarlas. El motivo es obligatorio (mínimo 8 caracteres) y no se admiten horas futuras.

Los apuntes tienen que encajar en la secuencia real del empleado: no cabe una salida donde no había ninguna jornada abierta, ni una entrada sobre una jornada que ya lo estaba. Si algo no cuadra se responde 422 con el detalle y no se crea ninguna solicitud del envío, ni siquiera las que sí encajaban. Un lote con entrada y salida del mismo día se valida junto, así que la salida se apoya en la entrada que va con ella.

curl -X POST https://controlhorariolegal.com/api/v1/correcciones \
  -H "X-Api-Key: chl_tu_clave_aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "dni": "12345678Z",
    "motivo": "Olvidé fichar la salida del viernes",
    "apuntes": [
      { "tipo": "salida", "ts_solicitado": "2026-06-17 18:00" }
    ]
  }'
{ "ok": true, "n": 1, "ids": [341] }
{
  "ok": false,
  "error": "Ese apunte no encaja: el 17/06 a las 18:00 no había ninguna jornada abierta, así que ahí no puede ir una salida. Lo que encajaría es una entrada."
}

Para un solo apunte también se admite el formato plano { "tipo": "...", "ts_solicitado": "..." } sin el array.

POST /correcciones/resolver · requiere escritura

Aprueba o rechaza una solicitud. Aprobar inserta el fichaje que falta en la cadena de hashes, de ahí el permiso de escritura. Si quien aprueba desde tu portal no existe como usuario en Control Horario Legal, envía aprobador_nombre: queda escrito en la resolución para no perder la trazabilidad.

curl -X POST https://controlhorariolegal.com/api/v1/correcciones/resolver \
  -H "X-Api-Key: chl_tu_clave_aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "id_correccion": 341,
    "resolucion": "aprobado",
    "motivo": "Confirmado con el encargado",
    "aprobador_email": "rrhh@empresa.es",
    "aprobador_nombre": "Luis Martín"
  }'
{ "ok": true, "id": 341, "resolucion": "aprobado" }

§ 04 · Proyectos

Horas imputadas frente a horas previstas.

La referencia de un fichaje es texto libre y te sirve si solo quieres etiquetar. Si además necesitas comparar lo fichado con lo presupuestado, da de alta tus proyectos con sus horas previstas y esa comparación te la devolvemos hecha: no hace falta que la reconstruyas en tu hoja de cálculo.

Las horas de una jornada se atribuyen al proyecto del apunte de entrada. Las pausas se descuentan. Una jornada abierta sin salida no se cuenta: es un dato incompleto, y preferimos no sumarlo a inventar un cierre.

GET /proyectos

Tu maestro de proyectos. Parámetro opcional activos=1 para excluir los archivados.

{
  "ok": true,
  "total": 1,
  "proyectos": [
    {
      "id": 7,
      "codigo": "PRY-2026/014",
      "nombre": "Reforma Nave B",
      "horas_previstas": 120.0,
      "fecha_inicio": "2026-07-01",
      "fecha_fin": "2026-09-30",
      "activo": true
    }
  ]
}

horas_previstas vale null si el proyecto no tiene presupuesto de horas; entonces no se calcula desviación.

GET /proyectos/horas

Horas imputadas por proyecto en un periodo, con la desviación frente a las previstas y el porcentaje consumido. Es el endpoint que resuelve el caso de uso completo en una sola llamada.

  • desde, hasta — formato YYYY-MM-DD. Por defecto, los últimos 30 días. Máximo 366 días por consulta.
  • dni, email o id_empleado — limita el cálculo a un solo trabajador.
curl "https://controlhorariolegal.com/api/v1/proyectos/horas?desde=2026-08-01&hasta=2026-08-31" \
  -H "X-Api-Key: chl_tu_clave"

{
  "ok": true,
  "desde": "2026-08-01",
  "hasta": "2026-08-31",
  "proyectos": [
    {
      "id_proyecto": 7,
      "codigo": "PRY-2026/014",
      "nombre": "Reforma Nave B",
      "segundos": 411840,
      "horas": 114.4,
      "hhmm": "114:24",
      "horas_previstas": 120.0,
      "desviacion": -5.6,
      "porcentaje": 95.3
    }
  ],
  "sin_imputar": { "segundos": 28800, "horas": 8.0, "hhmm": "08:00" }
}
  • desviacion — horas reales menos previstas. En negativo, queda margen; en positivo, se ha pasado.
  • porcentaje — consumo sobre lo previsto. null si el proyecto no tiene horas previstas.
  • sin_imputar — horas trabajadas en el periodo que no están asignadas a ningún proyecto. Si esta cifra crece, es que falta imputar.

Los proyectos activos sin horas en el rango aparecen a cero, porque “cero horas imputadas” es justo lo que necesitas ver en un informe de seguimiento.

POST /proyectos/imputar · requiere escritura

Asigna un fichaje ya registrado a un proyecto, o lo desasigna. No modifica el fichaje — eso es imposible por diseño — sino el dato de gestión que cuelga de él. Aun así exige escritura, porque es el número sobre el que alguien acaba facturando.

  • id_fichaje — obligatorio.
  • codigo o id_proyecto — el proyecto destino. Enviar ambos vacíos o ausentes desimputa el fichaje.
  • motivo — opcional, queda en el registro de la imputación.
curl -X POST https://controlhorariolegal.com/api/v1/proyectos/imputar \
  -H "X-Api-Key: chl_tu_clave" \
  -H "Content-Type: application/json" \
  -d '{
    "id_fichaje": 9021,
    "codigo": "PRY-2026/014",
    "motivo": "Reasignado desde AppSheet"
  }'
{ "ok": true, "id_fichaje": 9021, "id_proyecto": 7, "id_imputacion": 512 }

Si el código no existe en tu maestro, la respuesta es 404. La imputación guarda histórico: la anterior no se borra, se sustituye por la nueva como vigente.


§ 05 · Registro de fichajes

Fichar desde tu propio portal.

Si tus trabajadores ya entran a diario en una extranet o intranet vuestra, puedes dejar que fichen desde ahí. Requiere una clave con permiso de escritura, que concedemos caso por caso tras revisar la integración. Escríbenos y lo vemos.

POST /fichajes · requiere escritura

  • tipo — obligatorio: entrada, salida, pausa_inicio o pausa_fin.
  • dni, email o id_empleado — obligatorio uno de ellos.
  • id_centro — opcional; por defecto, el centro del empleado.
  • origen — extranet si lo lanza el propio trabajador desde vuestro portal (queda distinguible en el panel), api en otro caso.
  • lat, lng, precision — opcionales. Si los envías, se guardan como evidencia.
  • ip, user_agent — opcionales: la IP de la petición es la de tu servidor, no la del trabajador. Si nos pasas las reales, se registran esas.
  • referencia — tu código de proyecto u obra, máximo 64 caracteres. Una referencia inválida devuelve 422: no la recortamos en silencio.
  • observaciones — texto libre, máximo 500 caracteres.
curl -X POST https://controlhorariolegal.com/api/v1/fichajes \
  -H "X-Api-Key: chl_tu_clave_aqui" \
  -H "Content-Type: application/json" \
  -d '{ "dni": "12345678Z", "tipo": "entrada", "origen": "extranet" }'
{
  "ok": true,
  "id": 9021,
  "empleado": 14,
  "tipo": "entrada",
  "ts": "2026-06-18 07:02:11.482",
  "hora_local": "09:02:11",
  "referencia": "PRY-2026/014",
  "duplicado": false,
  "hash": "a3f1c9..."
}

La API respeta la política del centro: si un empleado está configurado para fichar solo en el kiosco, la petición se rechaza con 409. Un fichaje repetido en pocos segundos se considera duplicado y no rompe la cadena: se devuelve duplicado: true.

§ 06 · Notas técnicas

Horas, límites y actualización.

Zona horaria

El campo ts va siempre en UTC, con precisión de milisegundos: es el valor sobre el que se calcula el hash del registro. Los campos fecha y hora vienen ya convertidos a la zona horaria del centro, para que no tengas que replicar esa conversión — es justo donde se cuelan los errores de una hora. Para volcar a una hoja de cálculo o a un informe, usa fecha y hora.

Límites

  • 180 unidades por minuto y 6.000 por hora por cada clave. No todas las llamadas gastan lo mismo: /ping, /empleados, /centros y /proyectos cuestan 1; /estado, /horario, las de correcciones y /proyectos/imputar cuestan 2; /horas y /presencia, 4; /fichajes y /proyectos/horas, 8. Son 22 consultas seguidas de las más caras por minuto: muy por encima de cualquier integración normal, y ahí está para que un bucle mal cerrado no te deje sin servicio.
  • Además hay un tope por IP y un corte por credenciales inválidas repetidas, pensados contra abuso externo. Una integración con la clave bien puesta no los alcanza.
  • /fichajes: máximo 366 días por consulta y 10.000 registros por respuesta.
  • /horario: máximo 62 días por consulta.
  • /proyectos/horas: máximo 366 días por consulta.
  • /correcciones/listar: últimas 100. /correcciones/pendientes: últimas 200.
  • Hasta 10 claves activas por empresa.

Cada respuesta incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset, para que tu integración sepa cuánto le queda sin tener que adivinarlo. Si agotas el margen, la respuesta es 429 con la cabecera Retry-After y un campo retry_after en el cuerpo, ambos en segundos: espéralos y reintenta.

Actualización de datos

No disponemos de webhooks salientes. Para mantener sincronizado un sistema externo, consulta /fichajes periódicamente con un rango acotado — cada 15 minutos si necesitas casi tiempo real, o una pasada nocturna del día anterior para informes. Para un panel de presencia en vivo, /presencia resuelve toda la plantilla en una sola llamada.

§ 07 · Errores

Respuestas de error.

Cuando algo falla, la respuesta es { "ok": false, "error": "..." } con el código HTTP correspondiente:

Código Significado
201Creado (fichaje registrado o corrección solicitada).
401Falta la API key o es inválida/revocada.
403Tu plan no incluye la API (disponible en Pro), o la clave es de solo lectura y el endpoint exige escritura.
404Recurso no encontrado (p. ej. empleado inexistente).
405Método HTTP no permitido en ese endpoint.
409Conflicto con la configuración del centro (p. ej. empleado en modo solo kiosco).
429Límite de uso superado. Reintenta pasados los segundos que indica Retry-After.
422Datos inválidos (fechas mal formadas, rango excesivo, tipo erróneo…).

¿Listo para integrar?

Genera tu API key desde el panel, en Integraciones. La API se incluye en el plan Pro y también durante los 30 días de prueba.

Ver planes →