Payroll
Three reads, answering three different questions: is this month settled (closures), who has a payslip for it (payslips), and what was declared into the run (variable elements).
Two warnings before anything else. The payslip read carries no amount at all and cannot be made to — see below. And the amount on a variable element sits behind two locks, of which your scope is only the first.
The restricted envelope used on variable elements is the one described under Withheld fields in
Companies, employees and contracts.
The three reads do not share an order
| Read | Order |
|---|---|
| Closures | Most recent month first |
| Payslips | Most recent period first |
| Variable elements | Oldest period first |
This is not a mistake to work around silently: a consumer that pages the three the same way and stops early
gets the newest closures, the newest payslips and the oldest elements. Page each to nextCursor: null,
or sort on the period yourself.
Closures — is this month settled?
GET /api/v1/client/payroll-closures (scopes: payroll:read)
{
"items": [
{
"id": "dV3BQN",
"companyId": "1VaKPa",
"referenceMonth": "2026-03-01",
"periodFrom": "2026-02-21",
"periodTo": "2026-03-20",
"isClosed": true,
"isSynchronized": true
},
{
"id": "qJYA5J",
"companyId": "1VaKPa",
"referenceMonth": "2026-02-01",
"periodFrom": "2026-01-21",
"periodTo": "2026-02-20",
"isClosed": true,
"isSynchronized": false
}
],
"nextCursor": "LrKGZuyxy"
}
| Parameter | What it does |
|---|---|
from, to |
ISO dates matching the reference month by containment. |
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. |
A closure is the state of one payroll month for one establishment. It carries no personal data at all — no employee, no amount, no comment. This read tells you whether a month is settled, never what is in it.
referenceMonth and the period are two different windows
referenceMonth is the month the closure designates, normalised to its first day: 2026-04-01 is April
2026, and together with companyId it is what identifies the closure. Key your records on it.
periodFrom and periodTo are the dates that closure actually covers — and they are not derivable
from the reference month. A company paying in arrears closes April over a window that starts in March. Read
the period as the span, never as a restatement of the month.
The from/to filter matches the reference month, never the covered period. Matching the period would
answer months you did not ask for.
isSynchronized is the flag to branch on
| Flag | What it means |
|---|---|
isClosed |
No more input is accepted. A closed month can still change. |
isSynchronized |
The closure has been handed to the payroll software — that month is definitive. |
The two are independent. Do not infer one from the other, and do not consume a month as final because it is closed: consume it when it is synchronized.
There is exactly one closure per establishment and reference month, so a month missing from the listing has not been created yet rather than having been deleted.
This read triggers nothing. Distribution is a different operation, on a different scope.
Payslips
GET /api/v1/client/payslips (scopes: payslips:read)
{
"items": [
{
"id": "GNn7BV",
"employeeId": "GNnBNO",
"companyId": "1VaKPa",
"periodName": "Mars 2026",
"periodFrom": "2026-03-01",
"periodTo": "2026-03-31",
"isPaper": false,
"distributedOn": "<generated>",
"status": "Distributed",
"hasDocument": true
},
{
"id": "rPXa1N",
"employeeId": "rPX1No",
"companyId": "1VaKPa",
"periodName": "Mars 2026",
"periodFrom": "2026-03-01",
"periodTo": "2026-03-31",
"isPaper": false,
"distributedOn": null,
"status": "PendingDistribution",
"hasDocument": false
}
],
"nextCursor": "2XeZzfj0j"
}
| Parameter | What it does |
|---|---|
from, to |
ISO dates matching the period start by containment — ask for a month, get that month's run. |
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. |
One entry is one employee for one payroll period.
There is no amount on this read, and there cannot be one
No gross, no net, no net payable. The figures exist elsewhere in Staff & Go, but they hang off no key to this record: matching them would be a best-effort reconciliation already known to fail, so a number served here would sometimes be another employee's.
Do not infer, estimate or compute pay from this read. If you need the figures, take them from the PDF, which is the document the employer actually issued.
The employee is named by employeeId only
No name, no employee number, no social security number, no job function. None of them is withheld pending a
scope — they are not served at all, which is why they never appear in restricted either. Join on
employeeId against the employee read if you need an identity.
status is one code, picked by precedence
One row carries one code, the first that applies — not the full history. In order:
NoPayslip— no document was produced. This is tested first and wins over everything else, so a row that was distributed but carries no document readsNoPayslip, not a distribution code.PostalDistributed,ClientDistributionorDistributed— it reached the employee. The three say how, narrowest first: by post, by the company's own officer, or by Staff & Go.Downloaded— somebody fetched it from the screens. A payslip that reached its employee is described by that rather than by a download, which is why this ranks below the family above.PendingDistribution— none of the above.
Do not reconstruct this order by guesswork: NoPayslip at the bottom rather than the top is the mistake it
invites, and it is the one row where hasDocument and status must agree.
Several kinds of payslip are never served
In error, skipped, superseded by a corrected one, never reconciled to an employee, or belonging to an employee this API does not serve at all. Confidential and archived records are absent entirely — not reduced, absent — exactly as on the employee read.
So a count from this read is not a headcount, and an employee missing for a period means one of those cases, not an empty payroll.
Fetching the PDF
GET /api/v1/client/payslips/{id}/document (scopes: payslips:read)
hasDocument says whether there is a file to fetch. The route is deliberately not an MCP tool: the file
names a person, their social security number and their pay.
| Answer | What it means |
|---|---|
404 |
The payslip is not reachable — stop asking. |
503 + Retry-After |
It exists, but its bytes could not be fetched — come back. |
The two mean opposite things. And the 503 cannot say why: on a permanent loss of the stored file it
would repeat forever. Cap your retries and open a support ticket rather than looping.
Variable payroll elements
GET /api/v1/client/variable-payroll-elements (scopes: payroll:read)
{
"items": [
{
"id": "lNpkMP",
"employeeId": "GNnBNO",
"employmentId": "BVo9PW",
"companyId": "1VaKPa",
"natureCode": "HSUP",
"natureName": "Heures supplementaires",
"natureType": "Quantity",
"unit": "H",
"isPayrollExportable": true,
"period": "2026-03-01",
"value": null,
"restricted": [
{
"reason": "requires_scope",
"fields": [
"value"
]
},
{
"reason": "requires_company_opt_in",
"fields": [
"value"
]
}
]
},
{
"id": "OJyr0P",
"employeeId": "GNnBNO",
"employmentId": "BVo9PW",
"companyId": "1VaKPa",
"natureCode": "PRIME",
"natureName": "Prime exceptionnelle",
"natureType": "Amount",
"unit": "EUR",
"isPayrollExportable": false,
"period": "2026-03-01",
"value": null,
"restricted": [
{
"reason": "requires_scope",
"fields": [
"value"
]
},
{
"reason": "requires_company_opt_in",
"fields": [
"value"
]
}
]
}
],
"nextCursor": "0AWX5t9d6"
}
| Parameter | What it does |
|---|---|
from, to |
ISO dates matching the period by containment. |
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. |
One entry is what was declared on one employee, for one nature, for one payroll period: a bonus, an overtime count, a meal allowance. It is the input a run consumes, never its result — this read does not say what was paid.
value is meaningless without unit and natureType
The same 12 is twelve hours, twelve days or twelve euros depending on the nature. Adding values across
natures produces a number that is wrong and looks right.
| Field | What it tells you |
|---|---|
natureType |
Whether the value is money (Amount) or a count (Quantity). Always served. |
unit |
The exact unit the company configured (H, J, EUR). null only when the nature records none. |
Group by natureCode first, read the unit, then read the number. unit and natureType are never
withheld, whatever your credential carries — precisely so that a withheld amount still says what kind of
quantity was declared.
value sits behind two locks, and the scope is only the first
payroll:read:amounts is required in addition to payroll:read, which never implies it. On top of
it, each company decides for itself whether it opens its variable pay to its partners.
Granted the sub-scope, you still receive null for every row of a company that has not opened it. So one
page legitimately serves the amount for some rows and withholds it for others — never conclude from one
withheld row that your credential lacks the scope.
A withheld value comes back null and is named in the restricted block of that entry, with the
reason: requires_scope (ask the client administrator), requires_company_opt_in (only that company can
open it), requires_plan (the partner plan does not cover it). Several reasons can stand at once and they
all matter — different parties settle them, so clearing one leaves the others.
A null value not named in restricted means no amount was recorded. That is a different fact from one
you may not see.
The nature travels inline
natureCode, natureName and natureType come with each entry, and there is no referential of element
natures on this API. Map your own payroll headings on natureCode rather than looking for a nature id to
resolve.
isPayrollExportable says whether the nature is meant to reach the payroll software. Rows where it is false
are served all the same — what to do with them is your decision rather than ours.
employmentId is the current employment
It may be null, and it changes between two calls — the day that employee signs a new contract. An element
hangs on the employee and the period, so this pointer is not necessarily the employment in force during that
period.
The element is a historical fact; that pointer is not. Do not diff on it, and do not report a change of
employmentId as a change to the element.
When the page is short
An establishment that lacks the relevant module is absent from these reads without being named, and the answer
says so through the partial block — see When a listing is shorter than your mandate in
Discovering what you can call.
None of these three reads offers a modifiedSince filter: no maintained, indexed column backs one. Re-read
the window instead. A row that vanishes between two reads is absent, not deleted — there is no delta feed
and no tombstone.