Les routes de gestion des clés, sous /api/institutions/:institutionId/keys, demandent la session d'un administrateur de l'établissement — la capacité institution.manage : voir Conventions. Un éditeur partenaire les appelle aussi, avec sa clé mhp_…, sur les établissements qu'il a ouverts : il ne voit et ne révoque que les clés qu'il a créées lui-même. Voir Éditeurs partenaires.
L'usage d'une clé sur le reste de /api est décrit à la fin de cette page.
L'objet Clé
{
"id": "2d388508-08af-4066-bc9e-2a705426a434",
"label": "ENT — émargement",
"prefix": "Abdvt1rV",
"scopes": ["attendance:read"],
"partnerId": null,
"expiresAt": "2027-09-01T00:00:00.000Z",
"revokedAt": null,
"lastUsedAt": "2026-10-14T10:21:00.000Z",
"createdAt": "2026-09-02T08:00:00.000Z"
}
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant de la clé, le :keyId des routes |
label | string | Le nom donné à la clé, de 1 à 120 caractères |
prefix | string | Les 8 caractères qui suivent mhk_ dans la clé. Ils sont stockés en clair et identifient la clé dans un journal |
scopes | string[] | Les portées de la clé, sans doublon. Voir Portées |
partnerId | uuid ou null | L'éditeur partenaire qui a créé la clé ; null si l'établissement l'a créée lui-même |
expiresAt | datetime ou null | L'échéance de la clé ; null si elle n'en a pas |
revokedAt | datetime ou null | La date de la PREMIÈRE révocation ; null tant que la clé est active |
lastUsedAt | datetime ou null | Le dernier usage réussi à s'authentifier, noté à la minute près ; null si la clé n'a jamais servi |
createdAt | datetime | La date de création |
Le secret de la clé n'est jamais rendu, sauf une fois, à la création, dans le champ plaintext. Le serveur n'en conserve qu'une empreinte.
Portées
Chaque portée devient, à l'usage, une capacité : c'est elle que les routes exigent, et que GET /api/me rend dans capabilities. Toute clé a en plus institution.view, qui ouvre la lecture de son établissement.
Dans ce tableau, … vaut /api/institutions/:institutionId.
| Portée | Capacité | Routes ouvertes | Écrit des données |
|---|---|---|---|
attendance:read | attendance.read | GET …/lessons/:lessonId/attendance, .csv et .html | non |
schedule:read | schedule.read | GET …/lessons, GET …/lessons/:lessonId, GET …/lessons/conflicts | non |
schedule:write | schedule.write | PUT …/lessons, DELETE …/lessons/by-ref/:ref, et les autres écritures sous …/lessons | oui : peut créer, déplacer et annuler des cours, créer des salles et des classes |
roster:write | roster.write | PUT …/classes/by-ref/:ref/roster | oui : peut créer des comptes et obtenir leurs liens d'activation |
GET /api/me, GET /api/institutions et les lectures de l'établissement (GET …, GET …/centres, GET …/room-kinds) ne demandent aucune portée. Il n'existe pas d'autre portée ; une valeur inconnue est refusée à la création de la clé. La liste complète des routes et de ce qu'elles exigent est dans Conventions.
GET /api/institutions/:institutionId/keys
Liste les clés de l'établissement, révoquées comprises. Exige institution.manage ou partner.manage. Un éditeur partenaire ne reçoit que les clés qu'il a créées.
Aucun paramètre de requête.
Réponse 200 :
{
"keys": [ { "id": "…", "label": "ENT — émargement", "partnerId": null, "…": "…" } ],
"scopes": ["attendance:read", "schedule:read", "schedule:write", "roster:write"]
}
| Champ | Type | Description |
|---|---|---|
keys | Clé[] | Les clés, de la plus ancienne à la plus récente. Voir L'objet Clé |
scopes | string[] | Toutes les portées qui existent, dans cet ordre |
| Code | code | Cause |
|---|---|---|
403 | forbidden | Ni administrateur de l'établissement, ni l'éditeur qui l'a ouvert |
404 | not-found | Établissement inconnu, ou que l'appelant n'atteint pas |
POST /api/institutions/:institutionId/keys
Crée une clé et la rend en clair, une seule fois. Exige institution.manage ou partner.manage.
{
"label": "ENT — émargement",
"scopes": ["attendance:read", "schedule:write"],
"expiresAt": "2027-09-01T00:00:00Z"
}
| Champ | Type | Obligatoire | Règles |
|---|---|---|---|
label | string | oui | Espaces de bord retirés ; ne peut pas être vide ; tronqué à 120 caractères |
scopes | string[] | oui | Peut être vide. Chaque valeur doit être une portée existante ; les doublons sont retirés |
expiresAt | datetime ou null | non | Absent, null ou "" : sans échéance. Sinon, une date dans le futur |
Réponse 201 :
{
"key": {
"id": "2d388508-08af-4066-bc9e-2a705426a434",
"label": "ENT — émargement",
"prefix": "Abdvt1rV",
"scopes": ["attendance:read", "schedule:write"],
"partnerId": null,
"expiresAt": "2027-09-01T00:00:00.000Z",
"revokedAt": null,
"lastUsedAt": null,
"createdAt": "2026-09-02T08:00:00.000Z",
"plaintext": "mhk_Abdvt1rV_47R9xK2pQ…"
},
"notice": "Cette clé ne sera plus affichée. Copiez-la maintenant."
}
| Champ | Type | Description |
|---|---|---|
key | Clé | La clé créée, avec en plus le champ plaintext |
key.plaintext | string | La clé complète, à placer dans l'en-tête Authorization. Elle n'est plus jamais rendue |
notice | string | Un rappel destiné à la personne qui crée la clé |
| Code | Corps | Cause |
|---|---|---|
400 | donnez un nom à cette clé — sinon nul n’osera la révoquer | label absent ou vide |
400 | scopes attend une liste | scopes absent ou pas un tableau |
400 | portée inconnue : … — parmi … | Une valeur de scopes n'est pas une portée |
400 | expiresAt attend une date ISO | expiresAt n'est ni une chaîne ni null |
400 | expiresAt illisible | expiresAt n'est pas une date |
400 | une échéance déjà passée n’ouvrirait rien | expiresAt est dans le passé |
403 | portée réservée aux éditeurs partenaires sous accord de données personnelles : … | Un éditeur partenaire sans accord demande attendance:read ou roster:write |
Ces refus portent le code invalid (400) ou forbidden (403).
DELETE /api/institutions/:institutionId/keys/:keyId
Révoque une clé. Exige institution.manage ou partner.manage. La clé n'est pas supprimée : elle reste listée, avec sa date de révocation, et son journal reste consultable.
| Paramètre de chemin | Type | Description |
|---|---|---|
keyId | uuid | L'identifiant de la clé |
Réponse 200 :
{ "key": { "id": "…", "revokedAt": "2026-10-20T09:00:00.000Z", "…": "…" }
}
| Champ | Type | Description |
|---|---|---|
key | Clé | La clé révoquée |
La révocation est idempotente : révoquer une clé déjà révoquée rend 200 et garde la date de la première révocation. Une clé révoquée est refusée dès la requête suivante.
| Code | Corps | Cause |
|---|---|---|
404 | clé inconnue | Aucune clé de cet identifiant dans l'établissement, keyId mal formé, ou — pour un éditeur partenaire — une clé qu'il n'a pas créée |
GET /api/institutions/:institutionId/keys/:keyId/activity
Le journal de ce qu'une clé a fait, du plus récent au plus ancien. Exige institution.manage : l'administration de l'établissement seulement.
| Paramètre de chemin | Type | Description |
|---|---|---|
keyId | uuid | L'identifiant de la clé |
| Paramètre de requête | Type | Défaut | Bornes | Description |
|---|---|---|---|---|
limit | integer | 20 | 1 à 100 | Nombre de traces rendues |
offset | integer | 0 | 0 ou plus | Nombre de traces à sauter |
search | string | vide | 120 caractères | Cherche dans action, actorEmail, actorLabel et targetId, sans tenir compte de la casse |
Réponse 200 :
{
"rows": [
{
"id": "b0f3…",
"actorId": null,
"actorEmail": null,
"actorKeyId": "2d388508-08af-4066-bc9e-2a705426a434",
"actorLabel": "ENT — émargement",
"action": "GET /api/institutions/:institutionId/lessons/:lessonId/attendance.csv",
"targetType": "/api/institutions/:institutionId/lessons/:lessonId/attendance.csv",
"targetId": null,
"metadata": { "status": 200 },
"at": "2026-10-14T10:21:07.412Z"
}
],
"total": 1
}
| Champ | Type | Description |
|---|---|---|
rows | Trace[] | Les traces de la tranche. Voir L'objet Trace |
total | integer | Le nombre total de traces de cette clé qui répondent à search |
| Code | Corps | Cause |
|---|---|---|
403 | cet accès ne permet pas cela — il faut : institution.manage | L'appelant n'administre pas l'établissement |
404 | clé inconnue | Aucune clé de cet identifiant dans l'établissement, ou keyId mal formé |
L'objet Trace
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant de la trace |
actorId | uuid ou null | Le compte qui a agi ; null quand c'est une clé |
actorEmail | string ou null | L'adresse de ce compte, recopiée au moment de l'acte ; null pour une clé |
actorKeyId | uuid ou null | La clé d'établissement qui a agi ; null quand c'est un compte ou un éditeur partenaire |
actorLabel | string ou null | Le nom de la clé, recopié au moment de l'acte : il survit à la suppression de la clé. Pour un éditeur partenaire : éditeur partenaire « … » |
action | string | La méthode HTTP et le motif de la route, par exemple PUT /api/institutions/:institutionId/lessons |
targetType | string | Le motif de la route, sans la méthode |
targetId | string ou null | La valeur du paramètre :id, :shortId ou :accountId de la route ; null pour les autres, dont celles à :lessonId, :keyId ou :ref |
metadata | object | Le code de réponse, dans status (integer), et les champs simples du corps de la requête |
at | datetime | Le moment de l'acte |
Dans metadata, un tableau du corps est remplacé par "[N éléments]" et un objet par "[objet]". Les champs password, roomPassword, token, csv, secret et admin ne sont jamais inscrits.
Ce qui est inscrit. Pour une clé, chaque requête qui aboutit — code de réponse inférieur à 400 —, lectures comprises. Une requête refusée n'est pas inscrite, qu'elle le soit à l'authentification ou ensuite, par exemple faute de capacité.
Échecs d'authentification
Sur /api, la clé se présente dans l'en-tête Authorization: Bearer …, et une requête ne porte qu'un justificatif. Une clé mal formée, inconnue, révoquée ou expirée rend 401, avec un en-tête WWW-Authenticate :
| Situation | Code | Corps | code | En-tête WWW-Authenticate |
|---|---|---|---|---|
Ni clé ni session, sur une route qui demande un appelant (sous /api/institutions/:institutionId, la réponse est 404) | 401 | identification requise | auth.required | — |
En-tête Authorization qui n'est pas Bearer … | 401 | en-tête Authorization attendu : Bearer <clé> | auth.invalid-key | Bearer error="invalid_token" |
| Clé mal formée | 401 | clé d’intégration illisible | auth.invalid-key | Bearer error="invalid_token" |
| Clé inconnue, ou secret faux | 401 | clé d’intégration inconnue ou révoquée | auth.invalid-key | Bearer error="invalid_token" |
| Clé révoquée | 401 | clé d’intégration inconnue ou révoquée | auth.invalid-key | Bearer error="invalid_token" |
| Clé expirée | 401 | clé d’intégration expirée | auth.invalid-key | Bearer error="invalid_token" |
| Établissement de la clé en pause | 403 | l’accès de cet établissement est en pause : … | institution.paused | — |
Cookie de session et en-tête Authorization ensemble | 400 | une seule identité par requête : … | auth.ambiguous | — |
Une clé inconnue et une clé au mauvais secret reçoivent la même réponse, à dessein. Une clé expirée reçoit un message distinct : il faut la remplacer, pas la corriger.
Une clé valide qui appelle une route hors de ses portées reçoit 403, et le message nomme la capacité manquante :
| Code | Corps | code |
|---|---|---|
403 | cet accès ne permet pas cela — il faut : schedule.read | forbidden |
Une clé qui désigne un autre établissement que le sien reçoit 404, code not-found : pour elle, cet établissement n'existe pas.
Sur les anciennes adresses /api/integration/…, alias dépréciés de ces routes, une requête sans clé d'établissement rend 401, clé d’intégration requise, avec l'en-tête WWW-Authenticate: Bearer realm="MeetingHub".