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:

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:

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.