Contrats de travail par l'API
Déposer, lister et télécharger 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 | ✅ oui | POST …/people/{person_id}/contracts |
| Lire (liste) | ✅ oui | GET …/people/{person_id}/contracts |
| Lire (le fichier) | ✅ oui | GET …/contracts/{id}/download |
| Mettre à jour | ❌ non | déposer un nouveau contrat, puis retirer l'ancien depuis l'écran |
| Supprimer | ❌ non | écran Administration → Personnes → Contrats de travail |
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 déposer | idem | 403 INSUFFICIENT_SCOPE sur le dépôt |
| 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
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.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 }
}En pratique, 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.comusflow.com/api/v1/ma-salle/people?limit=100" \
| jq -r '.data[] | select(.email == "camille.blanc@example.org") | .id')
echo "person_id = $PERSON_ID"É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.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é, 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.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 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.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.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.
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 avec un corps différent | 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) | 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 : la clé d'idempotence est dérivée du contenu métier plutôt que tirée au hasard, si bien qu'un second passage rend la réponse mémorisée au lieu de créer un doublon.
#!/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.comusflow.com/api/v1/ma-salle"
AUTH=(-H "Authorization: Bearer $COMUSFLOW_TOKEN")
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. Résoudre la personne sur son email.
PERSON_ID=$(curl -sS "${AUTH[@]}" "$BASE/people?limit=100" \
| jq -r --arg e "$EMAIL" '.data[] | select(.email == $e) | .id')
[ -n "$PERSON_ID" ] || { echo "personne introuvable : $EMAIL" >&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 ;;
esacPour 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