Paie

Trois lectures, qui répondent à trois questions différentes : ce mois est-il arrêté (clôtures), qui a un bulletin dessus (bulletins), et qu'a-t-on déclaré en entrée du calcul (éléments variables).

Deux avertissements avant tout le reste. La lecture des bulletins ne porte aucun montant et ne peut pas en porter — voir plus bas. Et le montant d'un élément variable est derrière deux verrous, dont votre scope n'est que le premier.

L'enveloppe restricted employée sur les éléments variables est celle décrite sous Champs retirés dans Sociétés, salariés et contrats.

Les trois lectures n'ont pas le même ordre

Lecture Ordre
Clôtures Mois le plus récent d'abord
Bulletins Période la plus récente d'abord
Éléments variables Période la plus ancienne d'abord

Ce n'est pas une bizarrerie à contourner en silence : un consommateur qui pagine les trois de la même façon et s'arrête tôt obtient les clôtures les plus récentes, les bulletins les plus récents et les éléments les plus anciens. Paginez chacune jusqu'à nextCursor: null, ou triez vous-même sur la période.

Clôtures — ce mois est-il arrêté ?

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"
}
Paramètre Ce qu'il fait
from, to Dates ISO filtrant le mois de référence par inclusion.
establishmentId Restreint la page à UN établissement de votre périmètre. Omis, elle les lit tous.
cursor, limit Pagination. Renvoyez nextCursor ; n'arrêtez que lorsqu'il vaut null.

Une clôture est l'état d'un mois de paie pour un établissement. Elle ne porte aucune donnée personnelle — ni salarié, ni montant, ni commentaire. Cette lecture vous dit si un mois est arrêté, jamais ce qu'il contient.

referenceMonth et la période sont deux fenêtres différentes

referenceMonth est le mois que la clôture désigne, normalisé à son premier jour : 2026-04-01 est avril 2026, et c'est lui qui, avec companyId, identifie la clôture. Clavez vos enregistrements dessus.

periodFrom et periodTo sont les dates que cette clôture couvre réellement — et elles ne se déduisent pas du mois de référence. Une société en décalage de paie clôture avril sur une fenêtre qui commence en mars. Lisez la période comme l'étendue, jamais comme une reformulation du mois.

Le filtre from/to porte sur le mois de référence, jamais sur la période couverte : filtrer la période répondrait des mois que vous n'avez pas demandés.

isSynchronized est l'indicateur sur lequel brancher

Indicateur Ce qu'il signifie
isClosed Plus aucune saisie n'est acceptée. Un mois clos peut encore changer.
isSynchronized La clôture a été remise au logiciel de paie — ce mois est définitif.

Les deux sont indépendants. Ne déduisez pas l'un de l'autre, et ne consommez pas un mois comme définitif parce qu'il est clos : consommez-le lorsqu'il est synchronisé.

Il y a exactement une clôture par établissement et mois de référence : un mois absent de la liste n'a pas encore été créé, il n'a pas été supprimé.

Cette lecture ne déclenche rien. La distribution est une autre opération, sur un autre scope.

Bulletins

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"
}
Paramètre Ce qu'il fait
from, to Dates ISO filtrant le début de période par inclusion — demandez un mois, vous obtenez le calcul de ce mois.
establishmentId Restreint la page à UN établissement de votre périmètre. Omis, elle les lit tous.
cursor, limit Pagination. Renvoyez nextCursor ; n'arrêtez que lorsqu'il vaut null.

Une entrée, c'est un salarié pour une période de paie.

Il n'y a aucun montant sur cette lecture, et il ne peut pas y en avoir

Ni brut, ni net, ni net à payer. Les chiffres existent ailleurs dans Staff & Go, mais ils ne tiennent à aucune clé vers cet enregistrement : les rapprocher serait une réconciliation au mieux, déjà connue pour échouer — un nombre servi ici serait donc parfois celui d'un autre salarié.

Ne déduisez, n'estimez et ne calculez aucune rémunération depuis cette lecture. S'il vous faut les chiffres, prenez-les dans le PDF, qui est le document réellement émis par l'employeur.

Le salarié n'est désigné que par employeeId

Ni nom, ni matricule, ni numéro de sécurité sociale, ni fonction. Aucun n'est retiré en attente d'un scope — ils ne sont pas servis du tout, et c'est pourquoi ils n'apparaissent pas non plus dans restricted. Faites la jointure sur employeeId avec la lecture des salariés s'il vous faut une identité.

status est un code unique, choisi par priorité

Une ligne porte un code, le premier qui s'applique — pas l'historique complet. Dans l'ordre :

  1. NoPayslip — aucun document n'a été produit. Il est testé en premier et l'emporte sur tout le reste : une ligne distribuée mais sans document se lit NoPayslip, pas un code de distribution.
  2. PostalDistributed, ClientDistribution ou Distributed — il a atteint le salarié. Les trois disent comment, du plus restreint au plus large : par voie postale, par le responsable de la société, ou par Staff & Go.
  3. Downloaded — quelqu'un l'a téléchargé depuis les écrans. Un bulletin qui a atteint son salarié est décrit par cela plutôt que par un téléchargement, d'où ce rang sous la famille précédente.
  4. PendingDistribution — aucun des cas ci-dessus.

Ne reconstituez pas cet ordre au jugé : placer NoPayslip en bas plutôt qu'en haut est l'erreur qu'il appelle, et c'est la seule ligne où hasDocument et status doivent s'accorder.

Plusieurs sortes de bulletins ne sont jamais servies

En erreur, écarté, remplacé par un bulletin corrigé, jamais rapproché d'un salarié, ou appartenant à un salarié que cette API ne sert pas du tout. Les enregistrements confidentiels et archivés sont absents entièrement — pas réduits, absents — exactement comme sur la lecture des salariés.

Donc un comptage issu de cette lecture n'est pas un effectif, et un salarié manquant sur une période relève de l'un de ces cas, pas d'une paie vide.

Récupérer le PDF

GET /api/v1/client/payslips/{id}/document (scopes : payslips:read)

hasDocument dit s'il y a un fichier à récupérer. Cette route n'est délibérément pas un outil MCP : le fichier nomme une personne, son numéro de sécurité sociale et sa rémunération.

Réponse Ce qu'elle signifie
404 Le bulletin n'est pas atteignable — cessez de demander.
503 + Retry-After Il existe, mais ses octets n'ont pas pu être lus — revenez.

Les deux disent des choses opposées. Et le 503 ne peut pas dire pourquoi : sur une perte définitive du fichier stocké, il se répéterait indéfiniment. Plafonnez vos réessais et ouvrez un ticket au support plutôt que de boucler.

Éléments variables de paie

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"
}
Paramètre Ce qu'il fait
from, to Dates ISO filtrant la période par inclusion.
establishmentId Restreint la page à UN établissement de votre périmètre. Omis, elle les lit tous.
cursor, limit Pagination. Renvoyez nextCursor ; n'arrêtez que lorsqu'il vaut null.

Une entrée, c'est ce qui a été déclaré sur un salarié, pour une nature, pour une période de paie : une prime, un compte d'heures supplémentaires, une indemnité de repas. C'est l'entrée que le calcul consomme, jamais son résultat — cette lecture ne dit pas ce qui a été payé.

value ne veut rien dire sans unit et natureType

Le même 12 vaut douze heures, douze jours ou douze euros selon la nature. Additionner des valeurs de natures différentes produit un nombre qui est faux et qui a l'air juste.

Champ Ce qu'il vous dit
natureType Si la valeur est de l'argent (Amount) ou un décompte (Quantity). Toujours servi.
unit L'unité exacte configurée par la société (H, J, EUR). null seulement si la nature n'en porte pas.

Groupez d'abord par natureCode, lisez l'unité, puis lisez le nombre. unit et natureType ne sont jamais retirés, quoi que porte votre habilitation — précisément pour qu'un montant retiré dise quand même quelle sorte de quantité a été déclarée.

value est derrière deux verrous, et le scope n'est que le premier

payroll:read:amounts est exigé en plus de payroll:read, qui ne l'implique jamais. Par-dessus, chaque société décide pour elle-même d'ouvrir ou non ses variables de paie à ses partenaires.

Le sous-scope accordé, vous recevez malgré tout null sur chaque ligne d'une société qui ne l'a pas ouvert. Une même page sert donc légitimement le montant pour certaines lignes et le retient pour d'autres — ne concluez jamais d'une ligne retirée que votre habilitation n'a pas le scope.

Un value retiré revient null et est nommé dans le bloc restricted de cette entrée, avec son motif : requires_scope (demandez à l'administrateur du client), requires_company_opt_in (seule cette société peut l'ouvrir), requires_plan (le plan partenaire ne le couvre pas). Plusieurs motifs peuvent tenir en même temps, et ils comptent tous — ce ne sont pas les mêmes interlocuteurs qui les lèvent, donc en régler un laisse les autres.

Un value à null qui n'est pas nommé dans restricted signifie qu'aucun montant n'a été enregistré. C'est un fait différent d'un montant que vous n'avez pas le droit de voir.

La nature voyage en ligne

natureCode, natureName et natureType accompagnent chaque entrée, et il n'existe aucun référentiel des natures d'éléments sur cette API. Faites correspondre vos propres rubriques de paie sur natureCode plutôt que de chercher un identifiant de nature à résoudre.

isPayrollExportable dit si la nature est destinée à atteindre le logiciel de paie. Les lignes où il vaut false sont servies quand même — ce que vous en faites est votre décision, pas la nôtre.

employmentId est le contrat courant

Il peut être null, et il change d'un appel à l'autre — le jour où ce salarié signe un nouveau contrat. Un élément tient au salarié et à la période, donc ce pointeur n'est pas nécessairement le contrat en vigueur pendant cette période.

L'élément est un fait historique ; ce pointeur ne l'est pas. Ne faites pas de diff dessus, et ne rapportez pas un changement d'employmentId comme un changement de l'élément.

Quand la page est plus courte

Un établissement auquel le module concerné manque est absent de ces lectures sans être nommé, et la réponse le dit par le bloc partial — voir Quand une liste est plus courte que votre mandat dans Découvrir ce que vous pouvez appeler.

Aucune de ces trois lectures n'offre de filtre modifiedSince : aucune colonne maintenue et indexée ne le porte. Relisez la fenêtre à la place. Une ligne qui disparaît entre deux lectures est absente, pas supprimée — il n'y a ni flux de delta ni pierre tombale.