API d'intégration
🧾

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érationPar l'APIRoute
Créer✅ ouiPOST …/people/{person_id}/contracts
Lire (liste)✅ ouiGET …/people/{person_id}/contracts
Lire (le fichier)✅ ouiGET …/contracts/{id}/download
Mettre à jour❌ nondéposer un nouveau contrat, puis retirer l'ancien depuis l'écran
Supprimer❌ nonécran Administration → Personnes → Contrats de travail

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 déposeridem403 INSUFFICIENT_SCOPE sur le dépôt
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

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

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

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

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