Contrats de travail par l'API
Déposer, lister, télécharger et retirer un CDDU ou un GUSO depuis un script
Ce que l'API fait — et ce qu'elle ne fait pas
Un contrat de travail est une pièce d'emploi (CDDU ou GUSO) déposée par votre organisation au nom d'une personne : un fichier, un intitulé, un type et une période. Il est rattaché au couple organisation + personne, jamais à un concert ni à une salle — une même personne peut travailler pour plusieurs employeurs sans qu'aucun ne voie les contrats des autres.
| Opération | Par l'API | Route |
|---|---|---|
| Créer la personne destinataire | ✅ oui | POST …/people |
| Créer le contrat | ✅ oui | POST …/people/{person_id}/contracts |
| Lire (liste) | ✅ oui | GET …/people/{person_id}/contracts |
| Lire (le fichier) | ✅ oui | GET …/contracts/{id}/download |
| Savoir si un contrat couvre un engagement | ✅ oui | GET …/contracts/{id}/coverage |
| Retirer le contrat | ✅ oui | DELETE …/contracts/{id} |
| Mettre à jour en place | ❌ non | retirer le dépôt, puis en faire un nouveau — deux gestes, deux traces |
| Lister les engagements sans contrat | ❌ non | écran Contrats intervenants → Affectés sans contrat / À échéance |
| Départager deux homonymes sans n° GUSO | ❌ non | renseigner le n° GUSO sur la bonne fiche, puis POST …/people/match |
| Modifier ou supprimer une personne | ❌ non | écran Administration → Personnes |
Prérequis
| Condition | Comment la vérifier | Sinon |
|---|---|---|
| L'API d'intégration est activée pour l'organisation | un appel authentifié quelconque répond autre chose qu'un 403 | 403 FEATURE_DISABLED — demandez l'activation à l'administrateur de l'organisation. Le jeton, lui, est valide : ne le renouvelez pas |
Un jeton portant people.read | GET /whoami : la clé est dans scopes et dans effective_permissions | 403 INSUFFICIENT_SCOPE sur les trois lectures |
…et people.write pour créer une personne, déposer et retirer | idem | 403 INSUFFICIENT_SCOPE sur les trois écritures |
| Le propriétaire du jeton a ces droits sur toute l'organisation | son rôle n'est pas restreint à une ou deux salles | 403 — voir l'encadré ci-dessous |
Le reste — obtenir un jeton, le présenter, lire les erreurs, les quotas — est commun à toute l'API et décrit sur la page API d'intégration.
Étape 1 — retrouver la personne, ou la créer
Les trois routes de contrats sont ancrées sur un person_id. Cette collection est la table de résolution qui le donne : sans elle, le dépôt serait une route inutilisable.
/api/v1/{org_slug}/peoplepeople.readLes personnes rattachées à votre organisation, triées par nom.
| Champ | Type | Description |
|---|---|---|
id | integer | l'identifiant à reporter dans les routes de contrats |
first_name | string | prénom |
last_name | string | nom |
email | string | adresse — la seule clé de rapprochement fiable avec votre système |
Paramètres : limit (1 à 100, défaut 50) et offset. Le tri est fixe — nom, prénom, puis identifiant — et cette dernière clé est ce qui garantit qu'un parcours page par page ne répète ni n'omet une ligne.
export COMUSFLOW_TOKEN="cfat_…"
curl -sS -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people?limit=100" | jq{
"data": [
{ "id": 314, "first_name": "Camille", "last_name": "Blanc",
"email": "camille.blanc@example.org" },
{ "id": 271, "first_name": "Sacha", "last_name": "Duval",
"email": "sacha.duval@example.org" }
],
"pagination": { "limit": 100, "offset": 0, "total": 2, "has_more": false }
}Désigner la personne par son n° GUSO
C'est la voie à préférer : un homonyme recevrait le contrat d'un autre, un numéro GUSO désigne une seule personne. La réponse est zéro ou une personne, dans la forme exacte de GET /people, sans pagination : personne ne porte ce numéro, la liste est vide — ce n'est jamais une erreur « introuvable ».
/api/v1/{org_slug}/people/matchpeople.readRend la personne qui porte ce n° GUSO, ou une liste vide. Lecture : pas d'Idempotency-Key.
# Le numéro voyage dans le corps, jamais dans l'URL.
PERSON_ID=$(curl -sS -X POST "https://app.preprod.comusflow.com/api/v1/ma-salle/people/match" \
-H "Authorization: Bearer $COMUSFLOW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"guso_number": "1234567890"}' \
| jq -r '.data[0].id // empty')
echo "person_id = ${PERSON_ID:-inconnue}"{
"data": [
{ "id": 314, "first_name": "Camille", "last_name": "Blanc",
"nickname": null, "email": "camille.blanc@example.org" }
]
}L'égalité est exacte, après retrait des espaces qui entourent le numéro : ni préfixe, ni motif. Une personne d'une autre organisation qui porte ce numéro est indiscernable d'un numéro inconnu : {"data": []}. Liste vide ? Créez la personne par POST /people avec son guso_number. Tout autre champ que guso_number rend 422 INVALID_INPUT, sans jamais redire la valeur reçue. La route a son propre quota — 30 appels par minute, 300 par heure, par jeton, en plus du quota général.
Sans numéro GUSO, résolvez sur l'email plutôt que sur le nom, et gardez la correspondance de votre côté : le person_id est stable.
# L'email est la clé de réconciliation la plus sûre : deux personnes peuvent
# être homonymes, elles ne partagent pas une adresse.
PERSON_ID=$(curl -sS -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people?limit=100" \
| jq -r '.data[] | select(.email == "camille.blanc@example.org") | .id')
echo "person_id = $PERSON_ID"Si la résolution ne rend rien
Un GUSO nomme parfois quelqu'un que ComusFlow ne connaît pas encore. Votre script n'a alors pas à abandonner la ligne : il crée la fiche, et repart avec son person_id.
/api/v1/{org_slug}/peoplepeople.writeCrée une personne dans votre organisation. Écriture : Idempotency-Key obligatoire.
| Champ | Obligatoire | Description |
|---|---|---|
first_name | oui | prénom (1 à 255 caractères) |
last_name | oui | nom (1 à 255 caractères) |
nickname | non | nom d'usage (jusqu'à 255 caractères) — ni unique, ni clé de rapprochement |
email | non | adresse — un artiste déclaré au GUSO n'en a pas toujours. Quand elle est là, elle est une clé de rapprochement, après le n° GUSO |
guso_number | non | n° GUSO — en entrée seule, jamais renvoyé ; clé de rapprochement dans votre organisation |
# Personne ne répond à cette adresse : on la crée, puis on repart avec son id.
# La clé d'idempotence est DÉRIVÉE de l'identité visée, pas tirée au hasard :
# relancer le script ne peut donc pas créer deux fiches.
KEY=$(printf 'person|%s' "$EMAIL" | sha256sum | cut -d' ' -f1)
PERSON_ID=$(curl -sS -X POST "https://app.preprod.comusflow.com/api/v1/ma-salle/people" \
-H "Authorization: Bearer $COMUSFLOW_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d "{\"first_name\": \"Camille\", \"last_name\": \"Blanc\", \"email\": \"$EMAIL\"}" \
| jq -r .id){ "id": 314, "first_name": "Camille", "last_name": "Blanc",
"email": "camille.blanc@example.org" }La réponse est un 201 portant exactement l'objet que sert GET /people : vous pouvez enchaîner sur le dépôt sans second appel.
Étape 2 — déposer le contrat
/api/v1/{org_slug}/people/{person_id}/contractspeople.writemultipart/form-data — les champs dans le formulaire, la pièce dans file.
Le corps est multipart : ce n'est pas du JSON avec un fichier en base64. Les champs métier voyagent dans le formulaire, la pièce dans file. Tous sont obligatoires.
| Champ | Format | Règle |
|---|---|---|
contract_type | cddu ou guso | énumération fermée : toute autre valeur est refusée |
title | string, 1 à 255 | intitulé lisible ; les retours à la ligne sont normalisés |
period_start | YYYY-MM-DD | début de la période d'emploi |
period_end | YYYY-MM-DD | ne peut pas être antérieure à period_start |
file | binaire | PDF, JPEG, PNG ou WebP — 15 Mo maximum, non vide |
L'en-tête Idempotency-Key est obligatoire
Toute écriture doit en porter un : une chaîne de votre choix (255 caractères maximum), unique à l'intention que vous exprimez. Un UUID v4 fait l'affaire. Sans lui : 400 IDEMPOTENCY_KEY_REQUIRED.
Nous l'imposons au lieu de le proposer parce qu'un client machine rejoue. Un timeout réseau ne vous dit pas si votre dépôt a abouti, et un contrat en double ne produit aucune erreur : il ressemble à une maladresse de saisie et se découvre des mois plus tard, au contrôle. Rendre l'en-tête facultatif n'aurait protégé que les intégrations qui y pensent — or le doublon naît précisément chez celles qui n'y pensent pas.
export COMUSFLOW_TOKEN="cfat_…"
curl -sS -X POST \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts" \
-H "Authorization: Bearer $COMUSFLOW_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-F "contract_type=cddu" \
-F "title=CDDU novembre — régie lumière" \
-F "period_start=2026-11-02" \
-F "period_end=2026-11-08" \
-F "file=@contrat-signe.pdf"HTTP/1.1 201 Created
{
"id": 1204,
"person_id": 314,
"contract_type": "cddu",
"title": "CDDU novembre — régie lumière",
"period_start": "2026-11-02",
"period_end": "2026-11-08",
"created_at": "2026-11-02T09:41:07Z"
}| Situation | Réponse |
|---|---|
| clé neuve | 201 — le dépôt est exécuté |
| même clé, même corps, dépôt terminé | la réponse mémorisée à l'identique, avec l'en-tête Idempotency-Replayed: true — rien n'a été exécuté |
| même clé, même corps, dépôt encore en cours | 409 IDEMPOTENCY_IN_PROGRESS — réessayez dans un instant, avec la même clé |
| même clé, autre requête (autre personne, corps différent) | 409 IDEMPOTENCY_KEY_REUSED — changez de clé |
| clé rejouée plus de 24 h après | traitée comme neuve : le dépôt est exécuté |
| dépôt en échec (422, 500) | la clé est libérée : rejouez-la telle quelle |
import uuid
import requests
BASE = "https://app.preprod.comusflow.com/api/v1/ma-salle"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# La clé identifie l'INTENTION, pas la tentative : on la calcule une fois,
# hors de la boucle de retry, sinon chaque nouvel essai crée un doublon.
key = str(uuid.uuid4())
with open("contrat-signe.pdf", "rb") as handle:
response = requests.post(
f"{BASE}/people/314/contracts",
headers={**HEADERS, "Idempotency-Key": key},
data={
"contract_type": "cddu",
"title": "CDDU novembre — régie lumière",
"period_start": "2026-11-02",
"period_end": "2026-11-08",
},
files={"file": ("contrat-signe.pdf", handle, "application/pdf")},
timeout=30,
)
response.raise_for_status()
if response.headers.get("Idempotency-Replayed") == "true":
print("déjà déposé — rien n'a été exécuté cette fois-ci")
print(response.json()["id"])Un champ hors bornes rend un 422 qui nomme le champ fautif :
HTTP/1.1 422 Unprocessable Entity
{
"error": "Validation failed",
"code": "INVALID_INPUT",
"request_id": "5f2c…",
"fields": {
"period_end": ["La fin de période ne peut pas être antérieure à son début."]
}
}Étape 2 bis — vérifier que le contrat couvre un engagement
/api/v1/{org_slug}/people/{person_id}/contracts/{contract_id}/coveragepeople.readDit si un contrat que vous avez déposé tombe sur un engagement de sa personne.
La règle est celle de l'écran Contrats intervenants de l'application : même organisation, même personne, une date d'engagement entre le début et la fin du contrat, bornes incluses, à la salle du contrat — ou à n'importe quelle salle pour un contrat sans salle. Un engagement est une affectation de technicien à une feuille de route datée, ou une déclaration GUSO sur un concert daté, ni annulé ni en option. Rien de l'engagement n'est renvoyé : la réponse est un mot.
curl -sS \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts/1204/coverage" \
-H "Authorization: Bearer $COMUSFLOW_TOKEN"
# → {"coverage": "covered"}| coverage | Sens |
|---|---|
covered | au moins un engagement tombe dans le contrat |
not_covered | aucun, et votre jeton lit tout ce que le contrat pourrait couvrir |
not_judged | aucun de ce que votre jeton lit, mais une partie lui est fermée : les déclarations GUSO d'une salle où il ne détient pas guso.read. Cet état dépend des seules permissions du jeton : il ne dit pas si une telle déclaration existe |
Aucune tolérance. Un concert est daté du jour où il commence. Un concert du 25 juin fini à 1 h du matin : un contrat du 26 au 26 rend not_covered, un contrat du 25 au 26 rend covered. Si votre script reçoit not_covered pour cette raison, c'est la date du contrat qui est à corriger.
La réponse décrit l'état au moment de l'appel : une feuille de route posée ou retirée plus tard la change sans que le contrat change. Appelez la route une fois après chaque dépôt. Elle exige people.read à l'échelle de l'organisation ; guso.read est facultative. Une personne ou un contrat hors de votre organisation rend NOT_FOUND, comme un identifiant inconnu. Le quota est celui de l'API.
Ce que l'API ne sert pas. Pas de liste des engagements sans contrat : c'est de la présence nominative et datée de toute l'organisation, et l'écran Contrats intervenants la sert à l'humain qui agit. Pas de fait « a des engagements » sur la liste des personnes : il orienterait vers la fiche la plus ancienne sans désigner personne. Deux homonymes sans n° GUSO se départagent par un humain, qui renseigne le n° GUSO sur la bonne fiche ; la désignation par n° GUSO désigne ensuite la bonne personne exactement.
Étape 3 — lister les contrats d'une personne
/api/v1/{org_slug}/people/{person_id}/contractspeople.readLes contrats que VOTRE organisation a déposés pour cette personne.
| Champ | Type | Description |
|---|---|---|
id | integer | identifiant du contrat |
person_id | integer | la personne concernée |
contract_type | string | cddu ou guso |
title | string | intitulé donné au dépôt |
period_start | date | YYYY-MM-DD |
period_end | date | YYYY-MM-DD |
created_at | date-time | instant du dépôt, ISO 8601 UTC suffixé Z |
curl -sS -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts?limit=100" | jq{
"data": [
{
"id": 1204,
"person_id": 314,
"contract_type": "cddu",
"title": "CDDU novembre — régie lumière",
"period_start": "2026-11-02",
"period_end": "2026-11-08",
"created_at": "2026-11-02T09:41:07Z"
}
],
"pagination": { "limit": 100, "offset": 0, "total": 1, "has_more": false }
}Étape 4 — télécharger la pièce
/api/v1/{org_slug}/people/{person_id}/contracts/{contract_id}/downloadpeople.readRend le fichier lui-même, en pièce jointe, dans son format d'origine.
La réponse n'est pas du JSON : c'est le fichier, servi avec un Content-Disposition: attachment et son nom d'affichage. Les droits sont vérifiés avant le premier octet — un flux ne peut plus changer son code HTTP une fois commencé.
curl -sS -L -OJ \
-H "Authorization: Bearer $COMUSFLOW_TOKEN" \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts/1204/download"Un contract_id qui n'appartient pas à cette personne, ou qui a été déposé par une autre organisation, rend 404 NOT_FOUND — le même 404 qu'un identifiant inexistant.
Étape 5 — retirer un contrat, ou le remplacer
/api/v1/{org_slug}/people/{person_id}/contracts/{contract_id}people.writeRetire un dépôt de votre organisation. 204, sans corps.
C'est la route à appeler quand vous retraitez un document déjà déposé. Sans elle, un script qui repassait sur le même PDF n'avait que deux conduites, toutes deux mauvaises : ignorer la ligne en silence, ou déposer un doublon qu'aucune erreur ne signale et qui se découvre au contrôle, des mois plus tard.
curl -sS -X DELETE \
"https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts/1204" \
-H "Authorization: Bearer $COMUSFLOW_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-w '%{http_code}\n'
# → 204| Situation | Réponse |
|---|---|
| contrat déposé par votre organisation pour cette personne | 204 No Content, sans corps |
| contrat déposé par un autre employeur | 404 NOT_FOUND — indiscernable d'un identifiant inexistant |
| contrat rattaché à une autre personne | 404 NOT_FOUND |
jeton sans people.write | 403 INSUFFICIENT_SCOPE |
| même clé rejouée sur un autre contrat | 409 IDEMPOTENCY_KEY_REUSED — rien n'est supprimé |
| même clé rejouée après succès | le 204 mémorisé, avec Idempotency-Replayed: true — pas le 404 de « déjà supprimé » |
Remplacer, c'est deux gestes — il n'existe ni PATCH ni PUT sur un contrat :
# Retraitement d'un PDF déjà déposé : on retire, puis on redépose.
# La clé de RETRAIT est dérivée du contrat visé — deux exécutions du script
# ne peuvent donc pas retirer deux contrats différents par mégarde.
DROP_KEY=$(printf 'withdraw|%s' "$CONTRACT_ID" | sha256sum | cut -d' ' -f1)
curl -sS -X DELETE "$BASE/people/$PERSON_ID/contracts/$CONTRACT_ID" \
"${AUTH[@]}" -H "Idempotency-Key: $DROP_KEY" -o /dev/null -w '%{http_code}\n'
curl -sS -X POST "$BASE/people/$PERSON_ID/contracts" \
"${AUTH[@]}" -H "Idempotency-Key: $NEW_KEY" \
-F "contract_type=$TYPE" -F "title=$TITLE" \
-F "period_start=$START" -F "period_end=$END" \
-F "file=@$FILE"Les refus que vous rencontrerez
Toute réponse d'erreur porte la même enveloppe : error, code, request_id, parfois fields. Branchez votre code sur code, jamais sur le message : le message peut être reformulé, le code est figé.
| HTTP | code | Sur cette surface, cela veut dire | Que faire |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | vous avez déposé sans en-tête d'idempotence | poser Idempotency-Key et rejouer |
| 401 | INVALID_CREDENTIALS | jeton absent, inconnu, expiré, révoqué — indistinguables par conception | vérifier l'en-tête Authorization ; sinon, recréer un jeton |
| 403 | INSUFFICIENT_SCOPE | people.read ou people.write manque à l'intersection | details.required_permission nomme la clé ; comparer scopes et effective_permissions dans /whoami |
| 403 | FEATURE_DISABLED | l'API n'est pas activée pour cette organisation | demander l'activation ; ne pas renouveler le jeton, il est valide |
| 404 | NOT_FOUND | personne hors de votre organisation, contrat d'un autre employeur, ou org_slug qui n'est pas celui du jeton | vérifier l'org_slug, puis relister /people |
| 409 | IDEMPOTENCY_KEY_REUSED | cette clé a déjà servi pour une autre requête (méthode, chemin ou corps) | changer de clé — ne pas insister : le serveur refuse pour ne pas vous rendre la réponse d'un autre appel |
| 409 | IDEMPOTENCY_IN_PROGRESS | un appel concurrent porte la même clé | réessayer après un court délai, avec la même clé |
| 422 | INVALID_INPUT | champ manquant ou hors bornes, période inversée, fichier vide, trop lourd ou d'un format refusé | fields détaille champ par champ. Ce n'est jamais transitoire : ne pas rejouer tel quel |
| 429 | TOO_MANY_REQUESTS | quota dépassé (120 appels/minute, 3 000/heure, par jeton ; 30/minute et 300/heure sur /people/match) | attendre Retry-After secondes |
Le catalogue complet des codes, les quotas et les en-têtes RateLimit-* sont sur la page API d'intégration.
Recette complète
Le script ci-dessous fait les quatre gestes dans l'ordre et peut être relancé autant de fois qu'on veut : les clés d'idempotence sont dérivées du contenu métier plutôt que tirées au hasard, si bien qu'un second passage rend la réponse mémorisée au lieu de créer un doublon. Il traite aussi le cas d'une personne encore inconnue — la créer fait partie de la recette, ce n'est pas une exception à traiter à la main.
#!/usr/bin/env bash
# Dépose un contrat de travail, de bout en bout, sans doublon possible.
set -euo pipefail
: "${COMUSFLOW_TOKEN:?jeton absent}"
BASE="https://app.preprod.comusflow.com/api/v1/ma-salle"
AUTH=(-H "Authorization: Bearer $COMUSFLOW_TOKEN")
GUSO="1234567890"
EMAIL="camille.blanc@example.org"
TYPE="cddu"
TITLE="CDDU novembre — régie lumière"
START="2026-11-02"
END="2026-11-08"
FILE="contrat-signe.pdf"
# 1. Désigner la personne par son n° GUSO. C'est une lecture portée par un POST :
# le numéro voyage dans le corps, jamais dans une URL.
PERSON_ID=$(curl -sS -X POST "$BASE/people/match" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d "{\"guso_number\": \"$GUSO\"}" \
| jq -r '.data[0].id // empty')
# 1 bis. Inconnue ? On la crée, avec une clé dérivée de l'identité visée — deux
# exécutions ne peuvent donc pas produire deux fiches.
if [ -z "$PERSON_ID" ]; then
PERSON_KEY=$(printf 'person|%s' "$GUSO" | sha256sum | cut -d' ' -f1)
PERSON_ID=$(curl -sS -X POST "$BASE/people" "${AUTH[@]}" \
-H "Idempotency-Key: $PERSON_KEY" -H "Content-Type: application/json" \
-d "{\"first_name\": \"Camille\", \"last_name\": \"Blanc\", \"email\": \"$EMAIL\", \"guso_number\": \"$GUSO\"}" \
| jq -r .id)
fi
[ -n "$PERSON_ID" ] && [ "$PERSON_ID" != "null" ] || { echo "personne irrésolue : $GUSO" >&2; exit 1; }
# 2. Une clé DÉRIVÉE de l'intention, pas tirée au hasard : relancer ce script
# deux fois de suite ne peut alors pas créer deux contrats.
KEY=$(printf '%s|%s|%s|%s|%s' "$PERSON_ID" "$TYPE" "$TITLE" "$START" "$END" \
| sha256sum | cut -d' ' -f1)
# 3. Déposer.
HTTP=$(curl -sS -o /tmp/contract.json -D /tmp/contract.headers -w '%{http_code}' \
-X POST "$BASE/people/$PERSON_ID/contracts" \
"${AUTH[@]}" -H "Idempotency-Key: $KEY" \
-F "contract_type=$TYPE" -F "title=$TITLE" \
-F "period_start=$START" -F "period_end=$END" \
-F "file=@$FILE")
case "$HTTP" in
201)
if grep -qi '^idempotency-replayed: true' /tmp/contract.headers; then
echo "déjà déposé — aucune écriture cette fois-ci"
else
echo "contrat déposé : $(jq -r .id /tmp/contract.json)"
fi ;;
409) echo "dépôt concurrent en cours, réessayez dans un instant" >&2; exit 75 ;;
*) echo "échec ($HTTP) : $(jq -r '.code // "?"' /tmp/contract.json)" >&2; exit 1 ;;
esac
# 4. Le contrat déposé couvre-t-il un engagement ? Un avertissement, jamais un
# nouveau dépôt : un contrat « non couvert » se signale à un humain.
CONTRACT_ID=$(jq -r .id /tmp/contract.json)
COVERAGE=$(curl -sS "$BASE/people/$PERSON_ID/contracts/$CONTRACT_ID/coverage" \
"${AUTH[@]}" | jq -r '.coverage // "?"')
if [ "$COVERAGE" = "not_covered" ]; then
echo "AVERTISSEMENT : contrat $CONTRACT_ID sans engagement — à signaler à un humain" >&2
fiPour relire l'ensemble des contrats d'une personne, parcourez la collection en vous appuyant sur total, qui compte la collection entière avant fenêtrage :
# Parcourir tous les contrats d'une personne, page par page.
OFFSET=0
while : ; do
PAGE=$(curl -sS "${AUTH[@]}" \
"$BASE/people/$PERSON_ID/contracts?limit=100&offset=$OFFSET")
echo "$PAGE" | jq -c '.data[]'
TOTAL=$(echo "$PAGE" | jq -r '.pagination.total')
OFFSET=$((OFFSET + 100))
[ "$OFFSET" -lt "$TOTAL" ] || break
done