Worked time
What was recorded against each working day — minutes worked against minutes planned. This is what a payroll
integration consumes, and it is the one read where a null is routinely mistaken for a zero. The section
Not declared is not zero, below, is the one to read before anything else.
The grain is the day, not the punch
One entry is what was recorded for one date and one employment. Individual clock-in and clock-out events are a different kind of data and this API does not serve them: you receive the totals a day was closed on, never the sequence that produced them.
That is deliberate. A payroll run consumes days; a punch sequence describes someone's movements through their shift, which is a far more intrusive record for what an integration actually needs.
Listing days
GET /api/v1/client/time-entries (scopes: time-entries:read)
{
"items": [
{
"id": "WV5RJ7",
"employmentId": "ePloqJ",
"employeeId": "GNnBNO",
"companyId": "1VaKPa",
"date": "2026-01-12",
"realMinutes": 495,
"theoreticMinutes": 420,
"isDeclared": true,
"submittedOn": "<generated>",
"hasExcessiveWorkload": true
},
{
"id": "mNw9JY",
"employmentId": "ePloqJ",
"employeeId": "GNnBNO",
"companyId": "1VaKPa",
"date": "2026-01-13",
"realMinutes": null,
"theoreticMinutes": null,
"isDeclared": false,
"submittedOn": null,
"hasExcessiveWorkload": false
}
],
"nextCursor": "pgyk3FWzA"
}
| Parameter | What it does |
|---|---|
from, to |
ISO dates bounding the day. Either may be omitted. |
establishmentId |
Narrows the page to ONE establishment of your perimeter. Omitted reads all of it. |
cursor, limit |
Paging. Pass nextCursor back; stop only when it is null. |
Days come out oldest first.
The window matches by containment, not overlap. A day is one date and belongs to exactly one window — unlike an absence, which spans several and can straddle a month boundary. Asking for January returns the days of January, nothing more. This differs from Leave and balances on purpose: the two reads answer questions of different shapes, and matching them the same way would be wrong on one of them.
An unparseable date leaves that side of the window open rather than failing the call. You may receive more than you asked for, never less.
establishmentId narrows, it never widens. Naming an establishment outside your perimeter returns nothing
rather than reaching it, and so does a value that is not an identifier this API issued — a stale bookmark gives
you an empty page, not an error and not the whole perimeter.
There is no modifiedSince filter. Nothing maintained and indexed backs one, so accepting it would mean
quietly ignoring it. Poll on the window instead — it is what a payroll period actually is.
Not declared is not zero
| Field | What it tells you |
|---|---|
realMinutes |
Minutes actually worked. null while the day is not declared. |
theoreticMinutes |
Minutes the schedule planned. null on the same terms. |
isDeclared |
Whether the day is declared: true only when both totals AND submittedOn are set. |
submittedOn |
When the period carrying this day was submitted. null while it has not been. |
realMinutes: null means the day has NOT been declared. It does not mean zero minutes were worked.
Reading it as 0 pays nothing for a day that has simply not been submitted yet, and nothing in the response
signals the mistake — you get a number where there was an absence of one. Branch on isDeclared, never on
realMinutes.
And the rule only runs one way. isDeclared is true when the two totals and submittedOn are all three
set — so a day can carry realMinutes: 420 and still read isDeclared: false, because it has not been
submitted. That is the ordinary case, not an edge one: the totals are there as soon as the day is filled
in, submittedOn only once the period is actually submitted. A reader that infers declaration from a non-null
realMinutes therefore treats undeclared days as declared — the opposite mistake to reading null as zero,
and the more common of the two.
The totals also go back to null if the period is reopened or refused. So a day you read as declared last
week can legitimately read as undeclared today. That is not data loss and not a correction: it is the period
having gone back for another pass.
hasExcessiveWorkload is a recorded flag, not one derived from the totals. Do not try to reproduce it by
comparing realMinutes to theoreticMinutes: it is set on the day by Staff & Go's own rules, which take more
into account than the two numbers you can see here.
One employee, several employments
An employee may hold several employments, each with its own days. employmentId is what separates them.
Two entries on the same date for one employeeId are not a duplicate — they are two contracts worked on
the same day. Deduplicating on (employeeId, date) silently drops one of them, and the one you drop is
someone's paid work. Key on (employmentId, date) instead.
A day that disappears is absent, not deleted
There is no delta feed and no tombstone. A day that stops being returned between two reads simply stops being returned — nothing announces it. Re-read the window rather than waiting for a deletion event.
When the page is short
A listing covers your whole perimeter, but an establishment that does not subscribe to the time-management
module is absent from this read without being named. The answer says that it is short, through the partial
block — see When a listing is shorter than your mandate in
Discovering what you can call.
This one matters here more than elsewhere: a perimeter mixing establishments that use Staff & Go for time management with establishments that do not is an ordinary situation, not an anomaly.