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.
| Surface | Préfixe | Authentification | Contrat publié | Pour qui |
|---|---|---|---|---|
| API d'intégration | /api/v1 | jeton cfat_ | ✅ oui, versionné | scripts, cron, CI, outils tiers |
| API publique des événements | /api/public | aucune | ❌ non | sites web qui affichent un agenda |
| API d'administration | /api/{org}/admin | session navigateur | ❌ non | l'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 :
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.connectez-vous à ComusFlow, puis ouvrez Préférences → Mes jetons d'API ;
- 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.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.choisissez une expiration. Un jeton qui n'expire jamais est un jeton qu'on oublie ;
- 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.
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" | jqTrois 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.
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é.
{
"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 :
| Constat | Signification |
|---|---|
la clé est dans scopes et dans effective_permissions | tout va bien |
la clé est dans scopes mais absente d'effective_permissions | le 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 deux | le 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
| Route | Permission | Rend |
|---|---|---|
GET /status | aucune | la version de l'API — ni version applicative, ni état de l'instance |
GET /openapi.json | aucune | le contrat OpenAPI 3.1 complet, statique |
GET /{org}/whoami | authentifié | identité, scopes et permissions effectives du credential |
GET /{org}/organization | authentifié | nom, slug et salles lisibles — se situer en un appel |
GET /{org}/venues | concerts.read | la table de résolution des salles : le venue_id que portent les concerts |
GET /{org}/concerts | concerts.read | la programmation, filtrable par salle, dates, statut et updated_since |
GET /{org}/people | people.read | l'annuaire des personnes de l'organisation (4 champs) |
GET et POST /{org}/people/{id}/contracts | people.read / people.write | les 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 :
{
"data": [ … ],
"pagination": { "limit": 50, "offset": 0, "total": 137, "has_more": true }
}| Clé | Sens |
|---|---|
limit | 1 à 100, défaut 50 |
offset | ≥ 0, défaut 0 |
total | compte exact de la collection entière, avant fenêtrage |
has_more | dé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 :
{
"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.
| HTTP | code | Conduite à tenir |
|---|---|---|
| 400 | BAD_REQUEST | corriger l'appel |
| 400 | IDEMPOTENCY_KEY_REQUIRED | poser l'en-tête Idempotency-Key sur l'écriture |
| 401 | INVALID_CREDENTIALS | vérifier Authorization ; sinon le jeton est expiré, révoqué, ou son propriétaire a quitté l'organisation |
| 403 | INSUFFICIENT_SCOPE | details.required_permission nomme la clé manquante — puis /whoami |
| 403 | FEATURE_DISABLED | l'API n'est pas activée pour l'organisation : le jeton est valide, ne le renouvelez pas |
| 404 | NOT_FOUND | ressource absente, hors périmètre, ou autre organisation — indistinguables par conception |
| 405 | METHOD_NOT_ALLOWED | vérifier la méthode documentée |
| 409 | IDEMPOTENCY_KEY_REUSED | cette clé a déjà servi avec un corps différent : en changer |
| 409 | IDEMPOTENCY_IN_PROGRESS | un appel concurrent porte la même clé : réessayer avec la même clé |
| 413 | PAYLOAD_TOO_LARGE | réduire la requête |
| 415 | UNSUPPORTED_MEDIA_TYPE | type de contenu non accepté |
| 422 | INVALID_INPUT | fields détaille champ par champ. Ce n'est jamais transitoire : ne pas rejouer tel quel |
| 429 | TOO_MANY_REQUESTS | attendre Retry-After secondes |
| 500 | INTERNAL_ERROR | ré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être | Limite | Ce qu'elle borne |
|---|---|---|
| par minute | 120 appels | un emballement ; se résorbe en 60 secondes |
| par heure | 3 000 appels | un abus soutenu |
Chaque réponse porte les en-têtes de quota :
| En-tête | Sens |
|---|---|
RateLimit-Limit | plafond de la fenêtre annoncée |
RateLimit-Remaining | appels restants dans cette fenêtre |
RateLimit-Reset | secondes avant réinitialisation — une durée, pas une date : elle ne dépend pas de l'horloge de votre machine |
Retry-After | sur 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.
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
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_atdans 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.