💻

API d'intégration

Piloter ComusFlow depuis vos scripts, votre CI, vos outils

/api/v1 est une API machine-to-machine : scripts, tâches planifiées, intégrations tierces. Elle est authentifiée par un jeton personnel scopé, et son contrat est publié en OpenAPI 3.1.

🧾 Vous venez pour les contrats de travail ?

Déposer un CDDU ou un GUSO, lister les contrats d'une personne et récupérer les pièces a son guide dédié, de bout en bout.

Contrats de travail par l'API →

Est-ce bien cette API que vous cherchez ?

ComusFlow expose trois surfaces HTTP. Elles portent des noms proches et n'ont ni le même public, ni les mêmes garanties. Se tromper de surface est l'erreur la plus coûteuse d'une intégration, parce qu'elle ne produit aucune erreur : on obtient des données, simplement pas celles qu'on croit.

SurfacePréfixeAuthentificationContrat publiéPour qui
API d'intégration/api/v1jeton cfat_✅ oui, versionnéscripts, cron, CI, outils tiers
API publique des événements/api/publicaucune❌ nonsites web qui affichent un agenda
API d'administration/api/{org}/adminsession navigateur❌ nonl'application ComusFlow elle-même

Cette page documente la première : une API machine-to-machine, authentifiée par un jeton personnel scopé, avec un contrat versionné sur lequel vous pouvez vous appuyer.

Activer l'API pour votre organisation

L'API d'intégration est fermée par défaut. Tant qu'elle n'a pas été activée pour votre organisation, un credential parfaitement valide reçoit un 403 FEATURE_DISABLED. C'est un interrupteur côté organisation, pas un problème de jeton : ne le renouvelez pas, demandez l'activation à votre administrateur.

La route /api/v1/status ne demande aucun credential et permet de vérifier que vous parlez bien à la bonne API :

bash
curl -sS "https://app.comusflow.com/api/v1/status"
# → { "api_version": "v1", "status": "ok" }

Obtenir un jeton

Un jeton d'API est personnel : il appartient à un membre d'une organisation, et n'est jamais un « compte de service ». C'est ce qui garantit qu'il ne peut rien faire que son porteur ne puisse déjà faire.

  1. 1.connectez-vous à ComusFlow, puis ouvrez Préférences → Mes jetons d'API ;
  2. 2.donnez-lui un nom qui dit à quoi il sert (« CI synchro agenda ») : c'est ce nom que vous lirez le jour où il faudra en révoquer un ;
  3. 3.choisissez ses scopes — le sous-ensemble de vos permissions que le jeton pourra exercer. La liste proposée ne contient que ce que vous détenez vous-même ;
  4. 4.choisissez une expiration. Un jeton qui n'expire jamais est un jeton qu'on oublie ;
  5. 5.copiez le secret immédiatement.

Le nombre de jetons actifs par personne et par organisation est plafonné. Si la création est refusée pour cause de quota, révoquez ceux dont vous ne vous servez plus : c'est le but du plafond.

S'authentifier

Le secret se présente dans l'en-tête Authorization, et nulle part ailleurs.

bash
export COMUSFLOW_TOKEN="cfat_…"   # collé depuis l'écran de création

curl -sS -H "Authorization: Bearer $COMUSFLOW_TOKEN" \
  "https://app.comusflow.com/api/v1/ma-salle/whoami" | jq

Trois règles, et aucune n'est une préférence de style :

  • jamais dans une URL. Une URL est journalisée partout : chez nous, chez votre proxy, dans l'historique de votre shell, dans les traces de votre CI. Un jeton en paramètre est un secret publié — et aucune route ne le lit ailleurs que dans Authorization ;
  • jamais dans un navigateur. Cette API n'accepte aucune origine tierce (pas de CORS) : un appel depuis une page web supposerait de livrer le secret au navigateur du visiteur ;
  • jamais dans un dépôt Git. Variable d'environnement, secret de CI, gestionnaire de secrets — la forme attendue de tous les exemples de ces pages.

Le modèle de permissions — l'intersection

C'est la seule chose à comprendre avant tout le reste.

text
ce que le jeton peut faire =
      permissions ACTUELLES de son propriétaire, dans cette organisation
    ∩ scopes déclarés du jeton
    ∩ périmètre de la ressource (organisation, salles)

Chaque couche retranche ; aucune n'élargit. Deux conséquences :

  • un jeton ne peut jamais accorder plus que son propriétaire. Créer un jeton n'est pas une élévation de privilège, c'est une restriction volontaire ;
  • un jeton peut perdre un droit sans avoir été modifié. Les permissions sont évaluées à chaque requête, sur les droits actuels du propriétaire. S'il change de rôle, quitte une salle ou quitte l'organisation, le jeton perd le droit correspondant immédiatement. Il n'a pas de mémoire — c'est ce qui fait qu'un départ n'oblige pas à faire la chasse aux credentials.

Diagnostiquer : GET /whoami

C'est le premier appel à faire, et le seul à faire quand quelque chose est refusé.

json
{
  "account": { "id": 42, "email": "regie@ma-salle.fr" },
  "organization": { "slug": "ma-salle", "name": "Ma Salle" },
  "credential": {
    "kind": "api_token",
    "token_id": 7,
    "name": "CI synchro agenda",
    "prefix": "cfat_a3f2c1",
    "expires_at": "2027-01-31T00:00:00Z",
    "last_used_at": "2026-08-13T09:12:44Z"
  },
  "scopes": ["concerts.read", "people.read"],
  "effective_permissions": { "…": "…" }
}

Lisez l'écart entre les deux derniers champs :

ConstatSignification
la clé est dans scopes et dans effective_permissionstout va bien
la clé est dans scopes mais absente d'effective_permissionsle propriétaire a perdu ce droit depuis la création du jeton. Ce n'est pas le jeton qu'il faut recréer, c'est le droit qu'il faut rendre au compte
la clé n'est dans aucun des deuxle jeton n'a jamais demandé ce scope : créez-en un nouveau avec le bon périmètre

last_used_at est la valeur d'avant l'appel en cours : la question à laquelle il répond est « depuis quand ce jeton ne sert-il plus ? », pas « quelle heure est-il ? ».

Périmètre : organisation et salles

Le jeton porte son organisation ; l'URL ne fait que la confirmer. Toutes les routes métier ont la forme /api/v1/{org_slug}/…. Ce org_slug n'est pas un sélecteur, c'est une vérification : s'il ne correspond pas à l'organisation du jeton, la réponse est 404, indiscernable d'une organisation inexistante. Vous ne pouvez donc pas, même par erreur, pointer un script vers une autre organisation.

Le périmètre de salles est celui du propriétaire au moment de l'appel. Un slug de salle inconnu, ou connu mais hors périmètre, donne le même résultat : rien — une page vide sur un filtre, un 404 sur un détail. Jamais un 403, qui vous dirait si la salle existe.

Ce que la v1 expose

RoutePermissionRend
GET /statusaucunela version de l'API — ni version applicative, ni état de l'instance
GET /openapi.jsonaucunele contrat OpenAPI 3.1 complet, statique
GET /{org}/whoamiauthentifiéidentité, scopes et permissions effectives du credential
GET /{org}/organizationauthentifiénom, slug et salles lisibles — se situer en un appel
GET /{org}/venuesconcerts.readla table de résolution des salles : le venue_id que portent les concerts
GET /{org}/concertsconcerts.readla programmation, filtrable par salle, dates, statut et updated_since
GET /{org}/peoplepeople.readl'annuaire des personnes de l'organisation (4 champs)
GET et POST /{org}/people/{id}/contractspeople.read / people.writeles contrats de travail — guide détaillé

Les champs listés dans le contrat OpenAPI sont le contrat. Un champ peut s'y ajouter sans préavis ; aucun n'en sera retiré en v1. Ce qui n'y est pas — les heures de service, les montants de facture, les contacts nominatifs, les données RH — en est absent volontairement : les exposer ferait de chaque intégration une surface de fuite.

Formats et pagination

Toutes les réponses sont en JSON UTF-8. Les horodatages (*_at) sont en ISO 8601 UTC suffixé Z ; les dates en YYYY-MM-DD ; les heures en HH:MM. Les collections rendent toutes la même enveloppe :

json
{
  "data": [ … ],
  "pagination": { "limit": 50, "offset": 0, "total": 137, "has_more": true }
}
CléSens
limit1 à 100, défaut 50
offset≥ 0, défaut 0
totalcompte exact de la collection entière, avant fenêtrage
has_moredérivé de total — il ne peut pas mentir

Le tri est stable : l'identifiant ferme toujours l'ordre, quel que soit le tri demandé. C'est ce qui garantit qu'un parcours page par page ne répète ni n'omet une ligne.

Erreurs

Toute réponse de statut ≥ 400 porte la même enveloppe, y compris les 404 de routage :

json
{
  "error": "message lisible",
  "code": "CODE_STABLE",
  "request_id": "…",
  "details": { },
  "fields": { }
}

Branchez votre code sur code, jamais sur error : le message peut être reformulé, le code est figé — le changer serait un changement incompatible, interdit en v1. Citez le request_id dans tout signalement.

HTTPcodeConduite à tenir
400BAD_REQUESTcorriger l'appel
400IDEMPOTENCY_KEY_REQUIREDposer l'en-tête Idempotency-Key sur l'écriture
401INVALID_CREDENTIALSvérifier Authorization ; sinon le jeton est expiré, révoqué, ou son propriétaire a quitté l'organisation
403INSUFFICIENT_SCOPEdetails.required_permission nomme la clé manquante — puis /whoami
403FEATURE_DISABLEDl'API n'est pas activée pour l'organisation : le jeton est valide, ne le renouvelez pas
404NOT_FOUNDressource absente, hors périmètre, ou autre organisation — indistinguables par conception
405METHOD_NOT_ALLOWEDvérifier la méthode documentée
409IDEMPOTENCY_KEY_REUSEDcette clé a déjà servi avec un corps différent : en changer
409IDEMPOTENCY_IN_PROGRESSun appel concurrent porte la même clé : réessayer avec la même clé
413PAYLOAD_TOO_LARGEréduire la requête
415UNSUPPORTED_MEDIA_TYPEtype de contenu non accepté
422INVALID_INPUTfields détaille champ par champ. Ce n'est jamais transitoire : ne pas rejouer tel quel
429TOO_MANY_REQUESTSattendre Retry-After secondes
500INTERNAL_ERRORréessayer avec un délai croissant, puis signaler en citant le request_id

Quotas

Deux fenêtres, sur un compteur propre à votre jeton — pas à votre adresse IP. Deux intégrations d'un même client ne se pénalisent donc pas.

FenêtreLimiteCe qu'elle borne
par minute120 appelsun emballement ; se résorbe en 60 secondes
par heure3 000 appelsun abus soutenu

Chaque réponse porte les en-têtes de quota :

En-têteSens
RateLimit-Limitplafond de la fenêtre annoncée
RateLimit-Remainingappels restants dans cette fenêtre
RateLimit-Resetsecondes avant réinitialisation — une durée, pas une date : elle ne dépend pas de l'horloge de votre machine
Retry-Aftersur un 429 uniquement : secondes à attendre

Lisez RateLimit-Remaining et ralentissez avant d'atteindre zéro. Un 429 se traite en attendant, jamais en réessayant immédiatement : les échecs d'authentification répétés sont comptés séparément, et insister prolonge le blocage.

Écritures et idempotence

Les lectures sont ouvertes ; toute écriture (POST, PUT, PATCH, DELETE) exige un en-tête Idempotency-Key : une chaîne de votre choix (255 caractères maximum), unique à l'intention exprimée. Sans lui : 400 IDEMPOTENCY_KEY_REQUIRED.

bash
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" \
  -F "period_start=2026-11-01" -F "period_end=2026-11-30" \
  -F "file=@contrat.pdf"

Rejouée avec le même corps, une clé rend la réponse mémorisée à l'identique, accompagnée de Idempotency-Replayed: true — c'est cet en-tête, et non le code HTTP, qui doit décider chez vous d'envoyer une notification ou d'incrémenter un compteur. Avec un corps différent, elle rend 409 IDEMPOTENCY_KEY_REUSED. Une écriture en échec libère sa clé : seul le succès est mémorisé, pendant 24 heures.

Une clé appartient au credential qui l'a utilisée : deux jetons distincts peuvent employer la même chaîne sans se gêner.

Versionnage et compatibilité

La version est dans le chemin : /api/v1. Une v2 coexisterait avec la v1 ; elle ne la remplacerait pas en place.

Peut changer sans préavis

  • ajout d'un champ dans une réponse
  • ajout d'un paramètre optionnel
  • ajout d'une route
  • ajout d'une valeur à une énumération documentée comme ouverte

Ne changera jamais en v1

  • retrait ou renommage d'un champ
  • changement de type ou de sémantique
  • resserrement d'une validation
  • changement d'un code d'erreur
  • changement des valeurs par défaut de pagination

Écrivez donc un client tolérant : ignorez les champs que vous ne connaissez pas, ne validez pas en mode strict contre une copie figée du contrat. Une dépréciation éventuelle est annoncée par un en-tête Deprecation, tenue six mois au minimum, puis signalée par un Sunset un mois avant le retrait — lequel se fait en v2, jamais en v1.

Consommer le contrat dans un outil

bash
curl -sS "https://app.comusflow.com/api/v1/openapi.json" \
  | jq '.openapi, .info.version, (.paths | keys)'

Le spec s'importe directement dans Postman, Insomnia ou Bruno (import from URL), et se donne à openapi-generator pour produire un client typé.

Ce que nous attendons de vous

  • stockez le secret comme un mot de passe — jamais dans un dépôt, un ticket, un canal de discussion, une capture d'écran ;
  • un jeton par usage, avec les scopes minimaux : le jour où il fuite, c'est tout ce qui fuite ;
  • posez une expiration, et renouvelez avant l'échéance plutôt qu'après l'incident ;
  • en cas de doute, révoquez. La révocation est immédiate ; recréer un jeton coûte une minute ;
  • surveillez last_used_at dans Préférences → Mes jetons d'API : un jeton qui ne sert plus est un jeton à révoquer. Un administrateur d'organisation voit ceux de toute l'organisation et peut les révoquer — jamais en créer pour autrui, le secret ne devant transiter par personne d'autre que son porteur.

En cas de fuite avérée : révoquez d'abord, enquêtez ensuite.