Reportes
Dos rutas, el mismo motor. Nada se guarda: cada llamada calcula el rango desde las marcas, los horarios y las decisiones de ese momento, así que una marca aprobada esta mañana cambia la respuesta de esta tarde.
Las fechas y la zona horaria
date_from y date_to son fechas civiles en la zona horaria de la empresa, las dos incluidas, con hasta 92 días entre ellas. Cada respuesta dice cuál es esa zona en company_timezone (un nombre IANA como America/Guatemala); las horas dentro del Detalle son horas de reloj en ella.
Un día de trabajo es el día en que empieza un turno: un turno nocturno de las 22:00 del 31 a las 06:00 del 1 es del 31, y su marca de salida, tomada el 1, cuenta ahí.
Filtros
Las dos rutas reciben los mismos parámetros que la página de reportes de la app:
| Parámetro | Qué hace |
|---|---|
date_from, date_to | Obligatorios. El rango, los dos días incluidos. |
location | Empleados de estas sedes. Se repite para varias: location=<id>&location=<id>. |
department, position, employee | Lo mismo, por departamento, puesto o empleado. |
include_inactive | true agrega a los empleados que se fueron. Sin él solo vuelven los empleados activos hoy. |
Sin filtros vuelven todos los empleados activos de todas las sedes. Los ids para filtrar vienen en las propias filas (employee, location, department, position). Una sede que no es de la empresa es un 400 location_forbidden, nunca un reporte vacío.
Para un cierre mensual, mande include_inactive=true: alguien que se fue el 20 todavía tiene la mayor parte del mes por contar. Los días fuera del empleo de una persona no se cuentan.
El Resumen
GET /api/v1/reports/summary/ responde una fila por empleado, ordenadas por código, sin paginar. Una empresa de 2 000 personas recibe unos 1,2 MB.
Cada fila empieza por quién es la persona (employee, code, name y el id y el nombre de su location, department y position) y sigue con los totales del rango. Los días son conteos; los minutos, minutos enteros.
| Columna | Qué cuenta |
|---|---|
expected_days | Días con un turno esperado, también los abiertos y los futuros. |
expected_minutes | Lo que pedían esos turnos, menos sus pausas descontadas. |
worked_days | Días en que la persona vino: presente, incompleto o todavía en curso. |
late_days, late_minutes | Entradas tarde pasada la tolerancia del turno, y sus minutos. |
early_leave_days, early_leave_minutes | Salidas antes del fin del turno pasada su tolerancia, y sus minutos. |
late_break_count, late_break_minutes | Pausas que duraron más que sus minutos, y los minutos de más. |
absent_days | Días esperados sin ninguna marca una vez cerrada la ventana de marcaje. |
incomplete_days | Días con una marca faltante una vez cerrada la ventana de marcaje. |
worked_minutes | Todo lo trabajado, ordinario y extra. |
ordinary_minutes | Lo trabajado menos lo extra. |
extra_minutes | Lo que pasa de lo que pedía el turno, más un día libre trabajado, más el tiempo dentro de un feriado que la empresa cuenta como extra. |
no_schedule_days | Días sin horario: no se esperaba nada, las marcas cuentan igual. |
vacation_days, sick_days, permission_days, other_leave_days | Días con turno que un período liberó, por motivo. |
has_pending | Una marca del rango espera revisión: los números todavía pueden cambiar. |
Una tardanza o una salida temprana que la empresa aprobó como justificada ya cuenta desde el inicio o hasta el fin del turno: no aparece en late_minutes ni en early_leave_minutes, y no baja ordinary_minutes.
Lo que debe una persona
debe = expected_minutes - ordinary_minutes
En todo día con turno, los minutos ordinarios nunca pasan de lo que pedía el turno, así que esta resta es exactamente la suma de lo que faltó cada día: minutos de tardanza, salidas tempranas, ausencias, una pausa que se alargó. El tiempo extra de un día nunca paga una hora que faltó otro; esa es una regla que su sistema puede aplicar encima si quiere.
Léala con tres cuidados:
- Vale cuando
no_schedule_dayses 0. En un día sin horario no se esperaba nada y todo lo trabajado es ordinario, así que esos minutos bajarían la deuda. - Un día incompleto debe su turno entero hasta que la empresa ponga la marca que falta: Kabal no adivina a qué hora se fue alguien. Mire
incomplete_days. - Una fila con
has_pending: truetodavía puede cambiar cuando alguien revise la marca que espera.
Sobre los feriados: cuando un horario no da libre un feriado (una tienda que abre todos los días) y la empresa cuenta las horas de feriado como extra, la parte de un turno dentro del feriado no pide nada, así que no suma a expected_minutes, venga o no la persona. Si vino, esas horas son extra_minutes. Si no vino, el día sigue contando en expected_days y absent_days, y no debe nada.
El Detalle
GET /api/v1/reports/daily/ responde cada día de cada empleado, agrupado por empleado y paginado por empleado: 100 personas por página por defecto, hasta 200 con page_size, ordenadas por código.
curl -H "Authorization: Bearer $KABAL_API_KEY" \
"https://app.kabal-attendance.com/api/v1/reports/daily/?date_from=2026-10-01&date_to=2026-10-31&page=1"
{
"date_from": "2026-10-01",
"date_to": "2026-10-31",
"company_timezone": "America/Guatemala",
"count": 230,
"page": 1,
"pages": 3,
"page_size": 100,
"results": [
{
"employee": "0193a4b2-7c1e-7d40-9a51-3f2c8b6e1d07",
"code": "E001",
"name": "Ana López",
"days": [{ "date": "2026-10-01", "state": "present", "late_minutes": 12 }]
}
]
}
Pida page=1 y siga hasta pages. Una página después de la última es un 404 not_found. El orden es estable mientras nadie se contrate a mitad de la descarga; si count cambia entre páginas, empiece de nuevo.
Cada día dice qué se esperaba (expected: el turno con sus horas, qué lo decidió en source, como el horario, un feriado, un período o un plan semanal aprobado, el nombre del feriado, y en time_off_reason por qué se liberó un día con turno), qué leyó el motor (marks, cada marca que miró, contada o no, con el papel que le dio), los números del día y un state. Un día no lleva ids internos ni las notas que la empresa escribe en una excepción o un período: lo que viaja es el motivo.
El state del día:
state | Significado |
|---|---|
upcoming | Todavía sin marca, y la tolerancia del turno no ha pasado. |
late_no_mark | Todavía sin marca, pasada la tolerancia, con la ventana de marcaje abierta. |
in_progress | Falta la entrada o la salida, con la ventana de marcaje abierta. |
present | Vino, con entrada y salida. |
incomplete | Falta la entrada o la salida y la ventana de marcaje cerró. |
absent | Sin ninguna marca y la ventana de marcaje cerró. |
rest | Un día libre: descanso, un feriado que el horario respeta, un período. |
extra_day | Trabajó en un día libre: todo es extra. |
no_schedule | La persona no tenía horario ese día. |
labels agrega lo que el motor notó sin cambiar el estado, como marcas duplicadas, una pausa que se alargó, una tardanza justificada o un feriado trabajado; la referencia las lista todas. Cada fila de un día pesa cerca de 1 KB.
Cuándo pedir un mes
No hay una señal de "mes cerrado". Pídalo cuando RRHH de la empresa haya cerrado el mes en Kabal, y tenga en cuenta:
- El último día de trabajo del mes cierra con su ventana de marcaje: un turno nocturno del 31 termina la mañana del 1. Pedir el 1 a las 08:00 todavía puede verlo
in_progress. - Los días
upcomingein_progressya suman susexpected_minutes, como suman aexpected_days. Un mes pedido antes de terminar muestra una deuda que baja sola. - Una marca aprobada, una marca faltante agregada o una solicitud decidida después de su consulta cambian los números. Vuelva a pedir las filas con
has_pending: true, o el mes entero, antes de cerrar la planilla.
Un mes entero para 2 000 personas es el Resumen más 20 páginas del Detalle, muy dentro de los límites.