Discovering what you can call
Two reads tell you what your credentials may do and what the customer company actually holds, before you write a single write call: your profile and the referentials.
Labels come back in your language
Send Accept-Language and every human-readable label in the answer is rendered in that language —
contract types, leave type names, and the wording of a refusal. Supported: de-DE, en-US, es-ES,
fr-FR, it-IT, pl-PL, pt-PT, tr-TR. Anything else falls back to the default rather than failing:
a label in the wrong language is a smaller harm than no answer.
This changes nothing you should branch on. Codes — code on a leave type, hiringCode on a
professional status, codeIdcc on an agreement — are the same in every language, and they are what your
integration keys on. Only what you display moves.
Identifiers are opaque — and not unique across types
Every identifier this API serves — an employee, a contract, an establishment, a leave line, a payslip — is an opaque string. Store it and hand it back verbatim: do not build one, do not parse one, and do not assume that two of them are consecutive.
The same string can be a valid identifier in more than one read. One encoding is used for the whole API
and nothing in the string names what it designates, so a value that resolves as a contract on
GET /api/v1/client/employments/{id} can resolve at the same time as a leave line on
GET /api/v1/client/leave-requests/{id} — two unrelated records. Neither answer says so, because the
identifier carries no type.
What follows for your storage: keep each identifier with the read it came from, never in one shared
table of "ids", and never match identifiers of different kinds against one another. Passing an identifier to
the wrong read is not something this API can catch for you — it answers 404, or it answers a record that
has nothing to do with the one you meant, and both look like an ordinary answer.
Your profile
GET /api/v2/client/partner/profile (scopes: partner:read)
{
"partnerId": "partner-sample",
"tenantIds": [
"PB56eJ"
],
"grantedScopes": [
"external-api",
"partner:read",
"payroll:read",
"payroll:trigger",
"referentials:read",
"companies:read",
"employees:read",
"employments:read",
"leaves:read",
"beta:access"
],
"plan": "Standard",
"modules": [
{
"moduleId": 1,
"enabled": true,
"advanced": false
}
]
}
| Field | What it tells you |
|---|---|
partnerId, tenantIds |
The identity the API resolved from your token, and the customer establishments your calls act on. A credential can be mandated on several; how far below each of them it reaches is not published here. The reads answer on the whole perimeter: a listing covers every establishment it reaches, and an {id} belonging to any of them resolves. An establishment that does not have the module a read needs is absent from that read, without being named — but the answer says that it is short, through the partial block described below. |
grantedScopes |
The scopes actually present on the token — the fastest way to diagnose a 403. |
plan |
Your quota tier. See the caveat below. |
modules |
State of each Staff & Go module over your whole perimeter: enabled as soon as one establishment it reaches has it, plus advanced for the second, independent level some features require. It does not say which establishment holds what. |
plan is not a commercial tier yet. Every credential reports Standard today, because provisioning
does not set it — treat it as a placeholder, not as the plan you signed for. Your quota values come from
your Staff & Go contact and from the RateLimit headers, not from this field. See
Rate limits.
A module that is not enabled makes the capabilities behind it refuse your calls, so reading modules
first saves you a refusal. Which module gates which capability is not published yet; ask your Staff & Go
contact in the meantime.
GET /api/v1/client/partner/profile remains available: it returns partnerId, tenantIds and scopes
(renamed grantedScopes in v2) and none of the fields above. See Versioning.
tenantId became tenantIds, a list, on both versions when a credential came to be mandated on
several establishments. This is a breaking change, taken on v1 rather than deferred: a single value cannot
describe such a credential, and publishing one would have been wrong rather than merely incomplete.
When a field could not be determined
A field the API could not read comes back null and is named in unavailable, with the reason:
{
"modules": null,
"unavailable": [
{ "reason": "source_not_configured", "fields": ["modules"] }
]
}
reason |
What to do |
|---|---|
source_not_configured |
The upstream read is not wired in this environment. Retrying returns the same answer — contact us. |
source_unreachable |
It failed on the way out. Retry later. |
unavailable is absent when everything was determined. A null on its own never means "undetermined":
"modules": [] means the company has no module enabled, and that is a complete answer.
When a listing is shorter than your mandate
Your reads answer on the whole perimeter, but they only serve the establishments that hold the module they depend on. The others are absent, and the answer says so — otherwise a short listing would be indistinguishable from an honestly empty one, while the two call for opposite conduct:
{
"items": [],
"nextCursor": null,
"partial": { "reasons": ["module_not_subscribed"] }
}
Value in reasons |
What to do |
|---|---|
module_not_subscribed |
Some of your establishments do not hold the module this read depends on. Neither a scope nor a plan releases them: the customer company decides, and it is the one to ask. |
unspecified |
The answer is short and the API cannot say why. Treat it as any other reason to look at what your mandate covers. Nothing to retry. |
partial is absent when the answer covers your whole perimeter — so a complete answer is exactly what it was
before this block existed.
Three things worth keeping apart:
- It is not
nextCursor. The cursor says whether more rows follow;partialsays rows are missing and will not follow, however far you walk it. - It is not
restricted. That one is about fields withheld from a record you did receive, and it travels inside each entry.partialis about records you did not receive at all. - It never names the missing establishment. Saying so would tell you which of your customer's companies subscribe to which module. What you can act on is here: the answer is short, and why.
Leave types
GET /api/v1/client/referentials/leave-types (scopes: referentials:read)
{
"items": [
{
"id": 1,
"code": "SAMPLE_LEAVE",
"name": "Sample leave",
"isCustom": false,
"companyId": null
},
{
"id": 2,
"code": "SAMPLE_LEAVE",
"name": "Sample leave (company)",
"isCustom": true,
"companyId": "1VaKPa"
}
]
}
Reference the type by id, branch on code — name is a label the company can override, so matching on
it breaks the day someone renames it in Staff & Go.
A code is not unique in the response. A company can define its own type on top of the standard one
it inherits, and both carry the same code — as the example above shows. Indexing the payload by code
therefore silently drops an entry; isCustom (true for the company's own type) is what tells the two
apart. Key by id if you need a unique key.
companyId names the establishment that defined the type, and it is null on a standard one — the key is
always there, like name. Null does not mean an unknown establishment: it means nobody defined the type,
every establishment of your perimeter inherits the same entry, and it is returned once. When two
establishments each define their own type under one code, companyId is what tells those apart.
The whole perimeter comes back in one response: there is no filter and no paging on this endpoint yet.
Contract types
GET /api/v1/client/referentials/contract-types (scopes: referentials:read)
{
"items": [
{
"id": 1,
"name": "Sample contract type",
"isActive": true
},
{
"id": 2,
"name": "Sample retired contract type",
"isActive": false
}
]
}
| Field | What it tells you |
|---|---|
id |
The only safe anchor on this referential. |
name |
A label, to display. Never branch on it. |
isActive |
false for a type that can no longer be chosen on a new contract. |
This referential has no functional code. Unlike a leave type, Staff & Go stores none for a contract
type, so id is the only key to rely on and name is for display alone.
Retired types are returned, with isActive false. An employment signed years ago may reference one, and
omitting it would leave you holding an identifier you cannot resolve. So do not read a type's absence from a
picker as absence from this list.
This is a platform-wide referential, not the company's: the same list answers every partner. It comes back whole, with no filter and no paging.
Professional statuses
GET /api/v1/client/referentials/professional-statuses (scopes: referentials:read)
{
"items": [
{
"id": 1,
"code": "SAMPLE_STATUS",
"hiringCode": "SMP",
"name": "Sample professional status",
"isActive": true
}
]
}
| Field | What it tells you |
|---|---|
id |
Technical identifier. |
code |
The stable functional code — this is what you branch on. This referential has one, unlike contract types. |
hiringCode |
The key that matches collective agreements. See the next section. |
name |
Label, to display. |
isActive |
false for a retired status, returned all the same. |
Platform-wide, like contract types, and returned whole.
Collective agreements
GET /api/v1/client/referentials/collective-agreements (scopes: referentials:read)
{
"items": [
{
"id": 10,
"companyId": "1VaKPa",
"codeIdcc": "0000",
"codeCcn": null,
"codeJo": null,
"name": "Sample collective agreement",
"professionalStatusHiringCode": "SMP",
"isActive": true,
"isAgreementActive": true
},
{
"id": 10,
"companyId": "1VaKPa",
"codeIdcc": "0000",
"codeCcn": null,
"codeJo": null,
"name": "Sample collective agreement",
"professionalStatusHiringCode": null,
"isActive": true,
"isAgreementActive": true
},
{
"id": 10,
"companyId": "DJABPM",
"codeIdcc": "0000",
"codeCcn": null,
"codeJo": null,
"name": "Sample collective agreement",
"professionalStatusHiringCode": "SMP",
"isActive": false,
"isAgreementActive": true
}
]
}
Unlike the two above, this one carries the company's data: the answer differs from one partner to the next.
| Field | What it tells you |
|---|---|
id |
The agreement's identifier. Not a key of the response — see below. |
companyId |
The establishment applying it — the opaque identifier /companies serves. |
codeIdcc |
The IDCC code, the agreement's national identifier. |
codeCcn, codeJo |
Complementary codes, absent when not filled in. |
name |
The agreement's label. |
professionalStatusHiringCode |
The professional status this attachment covers. null means it covers every status. |
isActive |
Whether that establishment still applies this attachment. |
isAgreementActive |
Whether the agreement still exists nationally. |
An entry is an attachment, not an agreement. A company may apply different agreements to different
professional statuses, so two entries can share the same id — as the example above shows — and the triple
(companyId, id, professionalStatusHiringCode) is what identifies one. Indexing the payload by id
therefore silently drops entries — the same trap as leave-type codes, on a different key.
If your perimeter covers several establishments, you receive one entry per establishment. Two of them
applying the same agreement to the same professional status is two entries, told apart by companyId, each
with its own isActive: one establishment may have stopped applying an agreement the other still applies.
Read isActive as being about the establishment named beside it, never about your perimeter as a whole.
Match professional statuses on hiringCode, never on id. The two referentials sit in tables linked
one-to-many: no row of professional-statuses corresponds one-for-one to an attachment, and joining by id
would return a row — the wrong one — without signalling anything.
The two flags answer two independent questions. Knowing the company no longer applies an attachment says nothing about whether the agreement still exists nationally, and the other way round. Inactive attachments are returned so an older contract stays resolvable.
Company groupings
GET /api/v1/client/referentials/company-groupings (scopes: referentials:read)
{
"items": [
{
"id": "kNR1P3",
"name": "Unite legale Sample",
"code": "UL-SAMPLE",
"type": "LegalUnit",
"siren": "123456789",
"companyIds": [
"1VaKPa",
"DJABPM"
]
},
{
"id": "RP85Ng",
"name": "Groupement Sample",
"code": "GRP-SAMPLE",
"type": "Custom",
"siren": null,
"companyIds": [
"1VaKPa"
]
}
]
}
How the client organises its companies. Returns the groupings your perimeter belongs to, unpaged.
| Field | What it tells you |
|---|---|
id |
Opaque identifier of the grouping. |
name, code |
Labels the client can edit. Display them; never branch on them. |
type |
LegalUnit (companies sharing one SIREN) or Custom (a grouping the client defined). Branch on this. |
siren |
Present only on a LegalUnit, in clear — a public legal identifier. |
companyIds |
Which establishments of your perimeter belong to it — the identifiers /companies serves. Never empty. |
A grouping names a list of establishments, not one. Unlike every other read here, it carries no single establishment of origin, because a grouping is a set companies belong to rather than something one of them owns. Two of your establishments in one grouping is one entry naming both — not two entries.
This is not an org chart. It carries no reporting line, no manager and no employee, and groupings are
flat — there is no parent and no level. To find who reports to whom, read managerResourceId on the employee
(see Companies, employees and contracts).
It never says how many other companies a grouping holds. companyIds lists the members your mandate
covers and stops there: a grouping spanning companies you cannot read looks exactly like one that does not.
Do not read the length of that list as the size of the grouping.
Re-synchronising
None of these reads offers incremental synchronisation. There is no modifiedSince filter, and there
will be none in v1: nothing on the Staff & Go side carries an indexed last-modified date on these
resources. A filter that was accepted and returned everything would be worse than an absent one, since you
would believe you had received a delta.
Two consequences for your integration:
- Re-read the whole list to bring yourself up to date. These referentials are small and change rarely: it is a read, not a load.
- A vanished entry is simply absent. There is no deletion marker and no tombstone: the response says what exists, and nothing else. Compare it against your local copy to work out what disappeared.
For volumes that will not fit in one response — a large account's employees, say — bulk exports are planned. Your Staff & Go contact will tell you when they land.