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.