Exports en masse
Les lectures de cette API servent des pages. Elles n'ont volontairement pas de réponse à « donnez-moi les quatre mille salariés » : la pagination est le bon outil pour suivre une société au jour le jour, et le mauvais pour la première synchronisation, où elle coûte quatre-vingts appels et un curseur à tenir.
Les exports sont cette réponse. Vous demandez un fichier, nous le produisons en tâche de fond, vous le téléchargez une fois.
Les trois appels
POST /exports |
Demander un fichier. Répond immédiatement avec un export en Queued. |
GET /exports/{id} |
Où il en est. C'est ce que vous interrogez en boucle. |
GET /exports/{id}/content |
Le fichier lui-même, une fois le statut à Ready. |
Le corps et les réponses ci-dessous sont capturés depuis la suite de tests, pas recopiés : ce que vous voyez est ce que l'API a reçu et renvoyé.
POST /api/v1/client/exports
Idempotency-Key: 6a1f…-une-par-requete
Content-Type: application/json
{
"type": "employees"
}
{
"id": "1VaKPa",
"type": "employees",
"status": "Queued",
"failureReason": null,
"rowCount": null,
"contentLength": null,
"createdOn": "<generated>",
"readyOn": null,
"expiresOn": null
}
Interrogez GET /api/v1/client/exports/{id} toutes les quelques secondes : c'est peu coûteux et c'est
décompté sur votre godet habituel. Quand status vaut Ready, récupérez le contenu.
{
"id": "1VaKPa",
"type": "employees",
"status": "Ready",
"failureReason": null,
"rowCount": 2,
"contentLength": 353,
"createdOn": "<generated>",
"readyOn": "<generated>",
"expiresOn": "<generated>"
}
Ce que vous pouvez exporter
Le catalogue est une liste fermée de types nommés. Vous choisissez un type, vous ne composez jamais une requête.
| Type | Contenu | Scope également nécessaire |
|---|---|---|
employees |
Les lignes de GET /employees, en CSV |
employees:read |
Un type hérite du scope de la lecture qu'il extrait. exports:write dit que vous pouvez demander des
fichiers ; employees:read dit quelles lignes peuvent vous être remises. Demander un type dont vous n'avez
pas le scope de lecture est refusé sur-le-champ avec un 403 — pas par un job qui échoue dans la nuit.
Cela va au-delà de l'autorisation : le fichier est produit en parcourant cette lecture même, page par
page. Le bornage de périmètre, les salariés qui en sont exclus et les champs qui vous sont retirés sont ceux
que la liste applique déjà. Un export ne peut jamais vous remettre une ligne ou un champ que GET /employees
aurait retenu.
establishmentId restreint l'extraction à un établissement de votre périmètre. Comme partout ailleurs sur
cette API, il restreint et n'élargit jamais — et c'est la sortie de secours face à la limite de taille
ci-dessous.
Le fichier
employees est un CSV, et voici sa forme exacte pour que vous l'analysiez sans avoir à deviner :
- séparateur virgule, fins de ligne
CRLF, UTF-8 sans marque d'ordre des octets ; - échappement RFC 4180 — un champ n'est entre guillemets que s'il contient une virgule, un guillemet ou un saut de ligne, et un guillemet à l'intérieur d'un champ échappé est doublé ;
- une ligne d'en-tête, nommant les champs exactement comme la lecture JSON les nomme ;
- les booléens valent
true/false; une valeur absente est un champ vide ; - les identifiants sont les mêmes formes opaques que celles servies par
GET /employees: les deux se joignent.
La réponse est constituée d'octets sous text/csv, avec le nom de fichier dans Content-Disposition. Il n'y
a ni enveloppe JSON, ni base64, ni forme partielle, fragmentée ou reprenable de ce téléchargement.
⚠️ C'est une donnée d'intégration, pas un classeur. Une valeur saisie par votre client qui commence par
=, +, -, @, une tabulation ou un retour chariot est une formule pour un tableur : une telle valeur est
donc écrite précédée d'une apostrophe ('), qu'un tableur affiche comme du texte au lieu de l'évaluer.
Cela ne concerne que les champs saisis (fullName, civility, employeeNumber, professionalEmail,
companyName, jobFunction, managerFullName) ; les identifiants et isActive ne sont jamais modifiés, et
la jointure avec GET /employees tient. Si vous comparez un champ saisi à la lecture JSON, retirez cette
apostrophe de tête lorsqu'elle précède l'un de ces caractères.
La limite de taille est fonctionnelle
Une extraction dont le fichier dépasse la taille que cette API transmet en une seule réponse est refusée,
pas produite. L'export revient en Failed avec failureReason à TooLarge, et la route de contenu répond
alors 413.
Ce n'est pas un quota qu'on attend. Redemandez établissement par établissement — passez
establishmentId — ou paginez GET /employees. La limite est paramétrable et votre interlocuteur Staff & Go
peut vous indiquer la valeur en vigueur sur votre environnement.
Un échec porte toujours un nom
Un export Failed ne vous laisse jamais deviner. failureReason vaut l'une de ces valeurs :
| Motif | Ce qui s'est passé, et quoi faire |
|---|---|
PerimeterEmpty |
Tous les établissements désignés par votre mandat ont fermé depuis votre demande. Il n'y a plus rien à extraire. |
ModuleNotSubscribed |
Votre périmètre se résout toujours, mais aucun de ses établissements ne détient le module que ce type lit. C'est la société cliente qui l'ouvre. |
TooLarge |
Voir ci-dessus — restreignez la demande. |
Fault |
L'échec vient de chez nous. Redemandez. |
PerimeterEmpty mérite un mot. Vous l'obtenez à la place d'un fichier vide, volontairement : un fichier
vide est indiscernable d'une société qui n'a aucun salarié, et les deux appellent des conduites opposées.
Votre périmètre est résolu à la production du fichier
Pas au moment où vous le demandez. Un job attend en file, et un mandat peut être réduit ou révoqué pendant cette attente.
- Périmètre réduit entre-temps → le fichier ne contient que ce que votre mandat couvre encore, et un bloc
partialsur le statut indique que la réponse couvre moins que ce qui a été accordé à votre accréditation. - Périmètre devenu vide →
FailedavecPerimeterEmpty. - Votre périmètre est aussi revérifié à chaque téléchargement. Une accréditation qui ne couvre plus ce
pour quoi l'export a été créé obtient un
404— la même réponse qu'un identifiant inconnu, jamais un403, qui confirmerait l'existence de l'export.
Récupérez vos fichiers tant que votre mandat tient.
Sept jours, puis le contenu est purgé
Un export est conservé sept jours après sa mise à disposition. expiresOn dit exactement quand.
Passé ce délai, l'export répond toujours — avec Expired, pas 404. « Il a existé et il a expiré » et « il
n'a jamais existé » sont deux faits différents, et seul le premier vous dit d'en redemander un. rowCount et
contentLength survivent à la purge ; les octets, non. La route de contenu répond alors 410.
Demander deux fois
POST /exports exige un Idempotency-Key, comme toute écriture protégée de cette API — voir
Idempotence. Un rejeu de la même clé et du même corps vous rend l'export déjà créé,
plutôt que de mettre en file un second parcours de votre périmètre.
Il est également décompté sur un godet qui lui est propre, nettement plus serré que votre débit de plan. La création est l'acte coûteux ; le suivi et le téléchargement restent sur le godet habituel, donc ce n'est pas le polling qui vous fera plafonner.
Ce qu'un agent peut faire
create_export et get_export sont des outils MCP. create_export n'est pas idempotent : un agent doit vous
demander confirmation avant de l'appeler.
Le téléchargement n'est pas un outil, et ne le deviendra pas. Le résultat d'un agent est du texte qu'un modèle cite et recopie, et ce fichier nomme tous les salariés de votre périmètre. Récupérez-le depuis votre code, stockez-le comme un fichier, et ne le collez pas dans un prompt. Chaque appel est inscrit dans votre journal d'audit.