Skip to main content

Reports

Two routes, the same engine. Nothing is stored: every call computes the range from the marks, the schedules and the decisions of that moment, so a mark approved this morning changes this afternoon's answer.

Dates and the timezone​

date_from and date_to are civil dates in the company's timezone, both included, up to 92 days apart. Every answer says which timezone that is in company_timezone (an IANA name such as America/Guatemala); the times inside the detail are wall-clock times in it.

A work day is the day a shift starts: a night shift from 22:00 on the 31st to 06:00 on the 1st belongs to the 31st, and its exit mark, taken on the 1st, counts there.

Filters​

Both routes take the same query parameters as the reports page of the app:

ParameterWhat it does
date_from, date_toRequired. The range, both days included.
locationEmployees of these sites. Repeat it for several: location=<id>&location=<id>.
department, position, employeeThe same, by department, position or employee.
include_inactivetrue adds the employees who have left. Without it, only the employees active today come back.

Without filters you get every active employee of every site. The ids to filter by come in the rows themselves (employee, location, department, position). A site that is not the company's is a 400 location_forbidden, never an empty report.

For a monthly close, pass include_inactive=true: someone who left on the 20th still has most of the month to account for. The days outside a person's employment are not counted.

The summary​

GET /api/v1/reports/summary/ answers one row per employee, ordered by code, not paginated. A company of 2 000 people gets about 1.2 MB.

Each row starts with who the person is (employee, code, name, and the id and name of their location, department and position) and then the totals of the range. Days are counts; minutes are whole minutes.

ColumnWhat it counts
expected_daysDays with an expected shift, the open and future ones included.
expected_minutesWhat those shifts asked for, net of their deducted breaks.
worked_daysDays the person came: present, incomplete or still in progress.
late_days, late_minutesLate entries past the shift's tolerance, and their minutes.
early_leave_days, early_leave_minutesExits before the end of the shift past its tolerance, and their minutes.
late_break_count, late_break_minutesPauses that lasted longer than their minutes, and the minutes they went over.
absent_daysExpected days with no mark once the marking window closed.
incomplete_daysDays with a mark missing once the marking window closed.
worked_minutesEverything worked, ordinary and extra.
ordinary_minutesWorked minus extra.
extra_minutesOver what the shift asked for, plus a day off worked, plus time inside a holiday the company counts as extra.
no_schedule_daysDays without a schedule: nothing was expected, marks still count.
vacation_days, sick_days, permission_days, other_leave_daysDays with a shift that a leave freed, by reason.
has_pendingA mark of the range waits for a review: the numbers may still change.

A late arrival or an early leave that the company approved as justified is already counted from the start or to the end of the shift: it does not show in late_minutes or early_leave_minutes, and it does not lower ordinary_minutes.

What a person owes​

owed = expected_minutes - ordinary_minutes

On every day with a shift, ordinary minutes never pass what the shift asked for, so this subtraction is exactly the sum of what was missing each day: late minutes, early leaves, absences, a pause that ran long. Extra time on one day never pays back a missing hour on another; that is a rule your system can apply on top if it wants to.

Read it with three cautions:

  1. It holds when no_schedule_days is 0. On a day without a schedule nothing was expected and everything worked is ordinary, so those minutes would lower the debt.
  2. An incomplete day owes its whole shift until the company adds the missing mark: Kabal does not guess when someone left. Look at incomplete_days.
  3. A row with has_pending: true can still change when someone reviews the mark that waits.

About holidays: when a schedule does not give a holiday off (a shop that opens every day) and the company counts holiday hours as extra time, the part of a shift inside the holiday asks for nothing, so it adds nothing to expected_minutes, whether the person came or not. If they came, those hours are extra_minutes. If they did not, the day still counts in expected_days and absent_days, and owes nothing.

The detail​

GET /api/v1/reports/daily/ answers every day of every employee, grouped by employee and paginated by employee: 100 people per page by default, up to 200 with page_size, ordered by code.

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 }]
}
]
}

Ask for page=1, then up to pages. A page past the last one is a 404 not_found. The order is stable while nobody is hired in the middle of the download; if count changes between pages, start again.

Each day says what was expected (expected: the shift with its hours, what decided it in source, such as the schedule, a holiday, a leave or an approved week plan, the holiday's name, and in time_off_reason why a day with a shift was freed), what the engine read (marks, every mark it looked at, counted or not, with the role it gave it), the numbers of the day and a state. A day carries no internal ids and none of the notes the company writes on an exception or a leave: the reason is what travels.

The day's state:

stateMeaning
upcomingNo mark yet, and the shift's tolerance has not run out.
late_no_markNo mark yet, past the tolerance, with the marking window still open.
in_progressAn entry or an exit is still missing, with the marking window open.
presentCame, with an entry and an exit.
incompleteAn entry or an exit is missing and the marking window closed.
absentNo mark and the marking window closed.
restA day off: a rest day, a holiday the schedule respects, a leave.
extra_dayWorked on a day off: everything is extra.
no_scheduleThe person had no schedule that day.

labels adds what the engine noticed without changing the state, such as duplicate marks, a pause that ran long, a justified late arrival or a holiday worked; the reference lists them all. Each day row weighs about 1 KB.

When to pull a month​

There is no "month closed" signal. Pull when the company's HR has closed the month in Kabal, and keep in mind:

  • The last work day of the month closes with its marking window: a night shift of the 31st ends on the morning of the 1st. Pulling on the 1st at 08:00 can still see it in_progress.
  • upcoming and in_progress days already add their expected_minutes, as they add to expected_days. A month pulled before it ends shows a debt that goes down by itself.
  • A mark approved, a missing mark added or a request decided after your pull changes the numbers. Pull again the rows with has_pending: true, or the whole month, before closing payroll.

A whole month for 2 000 people is the summary plus 20 pages of the detail, well inside the limits.