Time tracking follows the employment period
Breaking change: clocking before an employee's hire date or after their termination date now answers 422 with subcode clocking_outside_employment_period. Daily balances add is_unscheduled and is_outside_employment, the monthly sheet and the team summary add a to_date block for the closed days, the employee schedule accepts a date, hire_date becomes editable, and reassigning or unassigning a schedule that has not started yet cancels it.
The time record now respects each employee's employment period: from
hire_date to termination_date, both days included. Until now the API
accepted clock entries on any date, the monthly sheet hid the minutes clocked on
days without a schedule, a wrong hire_date could not be corrected, and the
month balance of the current month mixed closed days with days still to come.
This release closes those gaps.
Breaking: clocking outside the employment period answers 422
Every write that creates or moves a clock entry now checks its date against the
employee's employment period. When the date falls before hire_date or after
termination_date, the request is rejected and nothing is written.
| Operation | Date that is checked |
|---|---|
POST /v1/time-entries/clock-in | occurred_at, or the server time when it is omitted. |
POST /v1/time-entries/manual | started_at and ended_at. |
POST /v1/time-corrections | The proposed time of an add_missing_entry (start and end) or adjust_time correction. No request is created. |
POST /v1/time-corrections/{time_correction}/approve | Checked again at approval time against the current employment period. The request stays pending. |
{
"error": {
"type": "invalid_request_error",
"code": "business_rule_violation",
"subcode": "clocking_outside_employment_period",
"message": "No se puede fichar el 04/05/2026: el empleado está de alta desde el 11/05/2026.",
"param": "occurred_at",
"doc_url": "https://docs.factuarea.com/guides/errors#business_rule_violation",
"request_id": "req_01HKQS5NTIMEOUTSIDEMPLOY01"
}
}The rule compares the calendar date of the timestamp in its own UTC offset, the
same date the time sheet assigns the entry to. Pausing, resuming and clocking
out of a span that is already open are never blocked, and neither are
remove_entry corrections or rejections.
What to do. If your integration clocks employees before their hire date, for
example because it creates them with a provisional date, correct hire_date
first with PUT /v1/employees/{employee} (see
Editable hire_date) and then send the entries. Branch on
error.subcode; the message is Spanish, human-facing text that names the hire
or termination date. See
clocking_outside_employment_period.
Daily flags: is_unscheduled and is_outside_employment
Each item of days[] in GET /v1/time-balances/monthly-sheet and
GET /v1/time-balances/employee/{employee} gains two booleans:
| Field | true when | Effect on the day |
|---|---|---|
is_unscheduled | The day is inside the employment period but no work schedule is in effect. | worked_minutes are the minutes actually clocked; expected_minutes, balance_minutes and overtime_minutes are 0. |
is_outside_employment | The day is before hire_date or after termination_date. | expected_minutes is 0 even if a schedule assignment covers the day; balance and overtime are 0. Entries recorded before this release still count as worked minutes. |
The two flags are never true at the same time. Before this release, a period
without any schedule reported 0 worked minutes, and minutes clocked on a day
without a schedule could count as overtime. Now those minutes always appear as
worked and never count as overtime. As a result, total_balance_minutes is the
sum of the daily balances and can differ from
total_worked_minutes − total_expected_minutes when minutes were clocked on
those days.
The frozen daily detail of a monthly close report does not include these flags, and closes that were already frozen keep their figures.
to_date versus the total_* fields
The total_* fields of GET /v1/time-balances/monthly-sheet and of each row of
GET /v1/time-balances/team-summary keep their meaning: they cover the whole
month. In the current month they are therefore a projection that already
subtracts the expected minutes of today and of the remaining days, so
total_balance_minutes shows a deficit until the month ends.
Both responses now add a to_date block with the accumulation of the closed
days, which means every day before today:
"to_date": {
"through_date": "2026-05-24",
"expected_minutes": 7200,
"worked_minutes": 7290,
"balance_minutes": 90,
"overtime_minutes": 60
}through_dateis the last closed day included, normally yesterday. Today is left out because its workday is still in progress.- In a finished month,
to_dateequals thetotal_*fields. - When no day of the month has closed yet (its first day, or a future month),
through_dateisnulland the four figures are 0. - The block is always present in both responses.
GET /v1/time-balances/employee/{employee}does not include it.
Show to_date.balance_minutes as the balance so far, and keep the total_*
fields for the projection of the month.
Employee schedule on a date
GET /v1/work-schedules/employee/{employee} accepts an optional date query
param (Y-m-d, today by default) and returns the schedule in effect on that
day. Use it to read a schedule that starts in the future, such as one assigned
from a pending hire date:
curl "https://api.factuarea.com/v1/work-schedules/employee/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c?date=2026-10-12" \
-H "Authorization: Bearer $FACTUAREA_API_KEY"Without a schedule in effect on that day it still answers
404 schedule_assignment_not_found. A date that is not a valid Y-m-d date
answers 422.
Editable hire_date
PUT /v1/employees/{employee} accepts hire_date (Y-m-d). When it is omitted,
it does not change. It must not be later than the employee's termination_date:
otherwise the answer is 422 parameter_invalid_value with subcode
invalid_hire_date and param termination_date. A malformed date also answers
422.
curl -X PUT https://api.factuarea.com/v1/employees/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4c" \
-d '{ "hire_date": "2026-09-01" }'Changing it never deletes anything. Existing clock entries and schedule
assignments are kept, and entries left outside the new employment period stay in
the ledger, flagged with is_outside_employment.
The error catalog now includes invalid_hire_date. Both
PUT /v1/employees/{employee} and POST /v1/employees/{employee}/deactivate
document it with a 422 example; deactivate already returned it when
termination_date was earlier than hire_date. See
invalid_hire_date.
Cancelling an assignment that has not started
POST /v1/work-schedules/{schedule}/assign and
POST /v1/work-schedules/{schedule}/unassign used to answer 422 when the
employee's open assignment started in the future and the new date was on or
before its start, for example when bringing forward a schedule assigned from a
wrong hire date. That assignment is now cancelled: it is closed as an empty
span (effective_to equal to its effective_from), kept in the history and
never in effect. An unassign without effective_to, which defaults to today,
cancels it too.
Moving back the start of an assignment that is already in effect, meaning one
that started today or earlier, still answers 422.
Monthly close
POST /v1/monthly-time-record-closes now includes every employee whose
employment period overlaps the month (hired on or before its last day and not
terminated before its first day), whatever their current status. An employee
terminated during the month appears in that month's report and payroll export;
one hired after the month is left out. The response and the seal format do not
change.
MCP
The tools share the same rules:
clock_in,record_manual_time_entry,request_time_correctionandapprove_time_correctionreturn the sameclocking_outside_employment_periodrejection.get_monthly_time_sheetandget_employee_time_balancereturn the daily flags;get_monthly_time_sheetandget_team_time_balance_summaryreturnto_date.get_employee_work_scheduleacceptsdate, andupdate_employeeacceptshire_date.assign_work_schedule,unassign_work_scheduleandclose_monthly_time_recordfollow the new assignment and close rules.
See the MCP tool catalog.
Updated endpoints13
| Endpoint | Description |
|---|---|
POST/v1/time-entries/clock-in | Clock in an employee |
POST/v1/time-entries/manual | Record a manual retroactive entry |
POST/v1/time-corrections | Request a time entry correction |
POST/v1/time-corrections/{time_correction}/approve | Approve a time entry correction |
GET/v1/time-balances/monthly-sheet | Retrieve an employee’s monthly time sheet |
GET/v1/time-balances/employee/{employee} | Retrieve an employee’s time balance for a period |
GET/v1/time-balances/team-summary | Retrieve the team time balance summary |
GET/v1/work-schedules/employee/{employee} | Get an employee’s schedule on a date |
PUT/v1/employees/{employee} | Update an employee |
POST/v1/employees/{employee}/deactivate | Deactivate an employee |
POST/v1/work-schedules/{schedule}/assign | Assign a schedule to an employee |
POST/v1/work-schedules/{schedule}/unassign | Unassign a schedule from an employee |
POST/v1/monthly-time-record-closes | Close a monthly time record |