Leave and balances

Absences and what remains on each counter. Why someone is absent is the most sensitive data this API carries, and it is withheld unless you were granted a scope for it — see Why an absence was taken, below.

The restricted envelope used here is the one described under Withheld fields in Companies, employees and contracts.

What a leave read returns: lines, not requests

One request can carry several lines. Three days of paid leave followed by two of RTT is one request with two lines, and each line has its own dates, its own quantity and its own type. This API serves lines, and requestId is what regroups those of one absence.

Without it, two lines of one request cannot be told from two requests filed on adjoining dates — and those are not the same fact. Group on requestId before you present anything to a user.

A page boundary can separate the lines of one absence. They come out in identifier order, so side by side — but nothing guarantees they land in the same page. Group on requestId after paging to the end, never page by page.

Listing absences

GET /api/v1/client/leave-requests (scopes: leaves:read)

{
  "items": [
    {
      "id": "WV5RJ7",
      "requestId": "ePloqJ",
      "employeeId": "GNnBNO",
      "companyId": "dV3QPr",
      "startDate": "2026-01-12",
      "endDate": "2026-01-15",
      "duration": 3.5,
      "durationUnit": "Days",
      "status": "Approved",
      "createdOn": "<generated>",
      "leaveType": null,
      "isWorkStoppage": null,
      "restricted": [
        {
          "reason": "requires_scope",
          "fields": [
            "leaveType",
            "isWorkStoppage"
          ]
        },
        {
          "reason": "requires_company_opt_in",
          "fields": [
            "leaveType",
            "isWorkStoppage"
          ]
        }
      ]
    },
    {
      "id": "mNw9JY",
      "requestId": "ePloqJ",
      "employeeId": "GNnBNO",
      "companyId": "dV3QPr",
      "startDate": "2026-01-12",
      "endDate": "2026-01-15",
      "duration": 3.5,
      "durationUnit": "Days",
      "status": "Approved",
      "createdOn": "<generated>",
      "leaveType": null,
      "isWorkStoppage": null,
      "restricted": [
        {
          "reason": "requires_scope",
          "fields": [
            "leaveType",
            "isWorkStoppage"
          ]
        },
        {
          "reason": "requires_company_opt_in",
          "fields": [
            "leaveType",
            "isWorkStoppage"
          ]
        }
      ]
    }
  ],
  "nextCursor": null
}
Parameter What it does
from, to ISO dates bounding the absence. Either may be omitted.
status One life-cycle code, or several separated by commas.
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.

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. Same parameter and same rule on the employee listing.

The window matches by overlap, not containment. An absence running from 28 January to 3 February is returned for January and for February. That is what you want when closing a payroll month: asking for absences wholly inside the month would drop exactly the ones that straddle the closure.

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.

A granted absence is not one status but three

An absence that has been granted enters Approved, becomes Treated once payroll has taken it, then Terminated. Filtering on Approved alone loses absences exactly as they become certain. For a planning view, ask for all three:

GET /api/v1/client/leave-requests?from=2026-01-01&to=2026-01-31&status=Approved,Treated,Terminated

The full set is SentForUpdate, ApprobationPending, Approved, Refused, TreatmentPending, Treated, Terminated, AbortionPending, Aborted, Imported. Unsubmitted drafts are never served: they are an employee's unsent intention, visible to nobody else in Staff & Go either.

A line that disappears is absent, not withdrawn

There is no delta feed and no tombstone. A request cancelled after you read it simply stops being returned — nothing announces it. Re-read the window rather than waiting for a deletion event, and do not treat a line you once saw as still active because no event said otherwise.

One absence

GET /api/v1/client/leave-requests/{id} (scopes: leaves:read)

Takes the id of a line, as returned by the listing.

Quantities always travel with their unit

Field What it tells you
duration How much absence the line is, half-days included — an afternoon off is 0.5.
durationUnit What that number counts: Days or Hours.

duration alone is meaningless. The same 7 is seven days or seven hours depending on the company's configuration for that leave type. Reading the number without the unit puts you wrong by roughly the length of a working day, in whichever direction you guessed. Never store or display one without the other.

Why an absence was taken

leaveType and isWorkStoppage sit behind leaves:read:reason, required in addition to leaves:read, which never implies it.

A leave type discloses sick or maternity leave, which is health data under Article 9 GDPR — the most sensitive category this API touches. So the default answer says when and how much, never why. A planning integration needs the dates; only an integration with a stated need should ever hold this scope.

The two fields are released together and withheld together. A partner holding only the work-stoppage flag could still separate sickness from holiday, so releasing either one alone would disclose what the pair exists to protect.

isWorkStoppage: null means withheld. It never means false. Reading it as false would state that the absence is not a work stoppage — a claim about someone's health, rather than the absence of one. Check restricted before you interpret it.

Balances

GET /api/v1/client/employees/{id}/leave-balances (scopes: leaves:read)

{
  "items": [
    {
      "employeeId": "GNnBNO",
      "companyId": "1VaKPa",
      "counterCode": "CP",
      "counterName": "Cong\u00E9s pay\u00E9s",
      "unit": "Days",
      "acquired": 25,
      "taken": 10,
      "remaining": 14.5
    },
    {
      "employeeId": "GNnBNO",
      "companyId": "1VaKPa",
      "counterCode": "RTT",
      "counterName": "RTT",
      "unit": "Days",
      "acquired": 12,
      "taken": 12,
      "remaining": 0
    }
  ]
}
Field What it tells you
employeeId Whose balance it is.
companyId The establishment they belong to — the opaque identifier /companies serves. Always present: an employee file belongs to exactly one.
counterCode Stable across companies (CP, RTT) — compare on this, not on counterName.
counterName A label, in your language. The company can rename it.
unit Days or Hours.
acquired, taken, remaining Accrued, consumed, left.

Never recompute remaining as acquired − taken. The three are maintained independently by the accrual engine: a counter that rolls over from another one, or one carrying a manual adjustment, does not satisfy that subtraction. Recomputing it makes you disagree with what the employee sees in Staff & Go, for exactly those employees. Read remaining as served.

Unpaged — a company has a handful of counters, and they all come back at once. There is no restricted envelope on this read: a balance says how much is left, never why an absence was taken, so there is nothing here to withhold.

An employee id that is not one of yours answers 404, not an empty list.