API d'intégration
🧾

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érationPar l'APIRoute
Créer la personne destinataire✅ ouiPOST …/people
Créer le contrat✅ ouiPOST …/people/{person_id}/contracts
Lire (liste)✅ ouiGET …/people/{person_id}/contracts
Lire (le fichier)✅ ouiGET …/contracts/{id}/download
Retirer le contrat✅ ouiDELETE …/contracts/{id}
Mettre à jour en place❌ nonretirer le dépôt, puis en faire un nouveau — deux gestes, deux traces
Modifier ou supprimer une personne❌ nonécran Administration → Personnes

Prérequis

ConditionComment la vérifierSinon
L'API d'intégration est activée pour l'organisationun appel authentifié quelconque répond autre chose qu'un 403403 FEATURE_DISABLED — demandez l'activation à l'administrateur de l'organisation. Le jeton, lui, est valide : ne le renouvelez pas
Un jeton portant people.readGET /whoami : la clé est dans scopes et dans effective_permissions403 INSUFFICIENT_SCOPE sur les trois lectures
…et people.write pour créer une personne, déposer et retireridem403 INSUFFICIENT_SCOPE sur les trois écritures
Le propriétaire du jeton a ces droits sur toute l'organisationson rôle n'est pas restreint à une ou deux salles403 — 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.

GET/api/v1/{org_slug}/peoplepeople.read

Les personnes rattachées à votre organisation, triées par nom.

ChampTypeDescription
idintegerl'identifiant à reporter dans les routes de contrats
first_namestringprénom
last_namestringnom
emailstringadresse — 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.

bash
export COMUSFLOW_TOKEN="cfat_…"

curl -sS -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
  "https://app.preprod.comusflow.com/api/v1/ma-salle/people?limit=100" | jq
json
{
  "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.

bash
# 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.

POST/api/v1/{org_slug}/peoplepeople.write

Crée une personne dans votre organisation. Écriture : Idempotency-Key obligatoire.

ChampObligatoireDescription
first_nameouiprénom (1 à 255 caractères)
last_nameouinom (1 à 255 caractères)
emailnonadresse — un artiste déclaré au GUSO n'en a pas toujours. Quand elle est là, elle est la clé de rapprochement
bash
# 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)
json
{ "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

POST/api/v1/{org_slug}/people/{person_id}/contractspeople.write

multipart/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.

ChampFormatRègle
contract_typecddu ou gusoénumération fermée : toute autre valeur est refusée
titlestring, 1 à 255intitulé lisible ; les retours à la ligne sont normalisés
period_startYYYY-MM-DDdébut de la période d'emploi
period_endYYYY-MM-DDne peut pas être antérieure à period_start
filebinairePDF, 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.

bash
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
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"
}
La réponse est le contrat déposé, sans enveloppe.
SituationRéponse
clé neuve201 — 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 cours409 IDEMPOTENCY_IN_PROGRESS — réessayez dans un instant, avec la même clé
même clé, corps différent409 IDEMPOTENCY_KEY_REUSED — changez de clé
clé rejouée plus de 24 h aprèstraité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
python
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
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

GET/api/v1/{org_slug}/people/{person_id}/contractspeople.read

Les contrats que VOTRE organisation a déposés pour cette personne.

ChampTypeDescription
idintegeridentifiant du contrat
person_idintegerla personne concernée
contract_typestringcddu ou guso
titlestringintitulé donné au dépôt
period_startdateYYYY-MM-DD
period_enddateYYYY-MM-DD
created_atdate-timeinstant du dépôt, ISO 8601 UTC suffixé Z
bash
curl -sS -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
  "https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts?limit=100" | jq
json
{
  "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

GET/api/v1/{org_slug}/people/{person_id}/contracts/{contract_id}/downloadpeople.read

Rend 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é.

bash
curl -sS -L -OJ \
  -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
  "https://app.preprod.comusflow.com/api/v1/ma-salle/people/314/contracts/1204/download"
-OJ demande à curl d'écrire le fichier sous le nom que le serveur annonce.

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

DELETE/api/v1/{org_slug}/people/{person_id}/contracts/{contract_id}people.write

Retire 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.

bash
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
SituationRéponse
contrat déposé par votre organisation pour cette personne204 No Content, sans corps
contrat déposé par un autre employeur404 NOT_FOUND — indiscernable d'un identifiant inexistant
contrat rattaché à une autre personne404 NOT_FOUND
jeton sans people.write403 INSUFFICIENT_SCOPE
même clé rejouée après succèsle 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 :

bash
# 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é.

HTTPcodeSur cette surface, cela veut direQue faire
400IDEMPOTENCY_KEY_REQUIREDvous avez déposé sans en-tête d'idempotenceposer Idempotency-Key et rejouer
401INVALID_CREDENTIALSjeton absent, inconnu, expiré, révoqué — indistinguables par conceptionvérifier l'en-tête Authorization ; sinon, recréer un jeton
403INSUFFICIENT_SCOPEpeople.read ou people.write manque à l'intersectiondetails.required_permission nomme la clé ; comparer scopes et effective_permissions dans /whoami
403FEATURE_DISABLEDl'API n'est pas activée pour cette organisationdemander l'activation ; ne pas renouveler le jeton, il est valide
404NOT_FOUNDpersonne hors de votre organisation, contrat d'un autre employeur, ou org_slug qui n'est pas celui du jetonvérifier l'org_slug, puis relister /people
409IDEMPOTENCY_KEY_REUSEDcette clé a déjà servi avec un corps différentchanger de clé — ne pas insister : le serveur refuse pour ne pas vous rendre la réponse d'un autre appel
409IDEMPOTENCY_IN_PROGRESSun appel concurrent porte la même cléréessayer après un court délai, avec la même clé
422INVALID_INPUTchamp 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
429TOO_MANY_REQUESTSquota 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 : 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.

bash
#!/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")

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')

# 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' "$EMAIL" | 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\"}" \
    | jq -r .id)
fi
[ -n "$PERSON_ID" ] && [ "$PERSON_ID" != "null" ] || { echo "personne irrésolue : $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 ;;
esac

Pour 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 :

bash
# 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