Ce qu'un éditeur partenaire fait sur /api avec sa clé :
Authorization: Bearer mhp_Q3x9LmPa_…
La clé a la même forme qu'une clé d'intégration, avec le préfixe mhp_. Elle ouvre deux sortes de routes :
- les routes de l'éditeur lui-même, qui exigent
une clé d’éditeurdans la table des routes : ouvrir un établissement, poser ses webhooks, lire sa mesure d'usage. Appelées par un autre appelant, elles répondent403,réservé aux éditeurs partenaires; - dans chaque établissement qu'il a ouvert, les routes qui exigent
institution.viewoupartner.manage: lire l'établissement, déclarer ses centres, poser ses clés, demander des liens d'entrée.
Elle n'ouvre ni l'emploi du temps, ni les feuilles de présence, ni les listes d'inscrits : pour cela, l'éditeur pose dans l'établissement une clé mhk_…, et son logiciel l'utilise ensuite. Voir Clés d'intégration.
Un établissement que l'éditeur n'a pas ouvert répond 404, comme s'il n'existait pas. Un établissement en pause ne lui ouvre plus que la lecture : ses écritures y sont refusées en 403, cet accès ne permet pas cela — il faut : institution.manage ou partner.manage. Chaque requête de l'éditeur dans un de ses établissements, lectures comprises, est inscrite au journal de cet établissement, au nom éditeur partenaire « … ».
Les corps JSON font 4 Mo au plus, et chaque éditeur fait 300 requêtes par minute au plus ; au-delà, la réponse est 429. Voir Conventions.
Échecs d'authentification
| Situation | Code | Corps | code |
|---|---|---|---|
| Clé mal formée | 401 | clé d’éditeur partenaire illisible | auth.invalid-key |
| Clé inconnue, secret faux, ou clé révoquée | 401 | clé d’éditeur partenaire inconnue ou révoquée | auth.invalid-key |
| Aucune clé, sur une route de l'éditeur | 401 | identification requise | auth.required |
| Cookie de session et clé ensemble | 400 | une seule identité par requête : … | auth.ambiguous |
Les réponses 401 d'une clé fautive portent l'en-tête WWW-Authenticate: Bearer error="invalid_token". Une clé d'éditeur n'a pas d'échéance : elle vaut jusqu'à ce que l'exploitant la révoque.
L'objet Établissement
{
"id": "6c1d3a52-7a0e-4f0b-9c55-0a54c3e1f2b8",
"slug": "lycee-bilingue-bonamoussadi",
"name": "Lycée bilingue de Bonamoussadi",
"kind": "school",
"status": "active",
"trialEndsAt": null,
"createdAt": "2026-10-01T08:00:00.000Z"
}
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant de l'établissement, le :institutionId des routes |
slug | string | Identifiant lisible, unique sur le service |
name | string | Le nom de l'établissement |
kind | string | school ou company |
status | string | trial (à l'essai : salles bornées à 10 personnes), active, ou paused (en pause : rien n'est effacé, rien ne s'ouvre) |
trialEndsAt | datetime ou null | La fin de l'essai ; null pour l'essai que vous ouvrez, qui n'expire pas |
createdAt | datetime | La date d'ouverture |
GET /api/me
Avec une clé d'éditeur :
{
"actor": { "kind": "partner-key", "id": "…", "name": "Votre logiciel" },
"institutions": [{ "id": "6c1d3a52-…", "name": "Lycée bilingue de Bonamoussadi", "status": "active" }],
"capabilities": ["institution.view", "partner.manage"]
}
actor.id et actor.name sont ceux de l'éditeur, pas de la clé. institutions liste les établissements qu'il a ouverts.
GET /api/institutions
Les établissements que vous avez ouverts, par ordre alphabétique.
Réponse 200 : { "institutions": [Établissement] }. Le status rendu tient compte d'un essai échu.
POST /api/institutions
Ouvre un établissement pour un de vos clients. Exige une clé d'éditeur.
{
"slug": "lycee-bilingue-bonamoussadi",
"name": "Lycée bilingue de Bonamoussadi",
"kind": "school",
"sandbox": true,
"admin": { "email": "direction@lycee.cm", "displayName": "Direction" }
}
| Champ | Type | Obligatoire | Règles |
|---|---|---|---|
slug | string | oui | Mis en minuscules. Lettres, chiffres et tirets, de 1 à 40 caractères, sans tiret au début ni à la fin |
name | string | oui | Espaces resserrés, tronqué à 120 caractères |
kind | string | non | school (par défaut) ou company |
sandbox | boolean | non | true ouvre l'établissement à l'essai (status: "trial"), sans échéance ; false par défaut |
admin.email | string | non | Le courriel du premier responsable de l'établissement |
admin.displayName | string | non | Son nom ; le courriel à défaut |
Le premier responsable reçoit le rôle promoter : le Promoteur, qui gouverne l'établissement. S'il n'a pas de compte, un compte est créé, sans mot de passe connu, avec un lien d'activation pour qu'il choisisse le sien.
Réponse 201 :
{
"organisation": { "id": "…", "slug": "…", "status": "trial", "…": "…" },
"admin": { "email": "direction@lycee.cm", "invited": true, "link": "https://reunion.exemple.cm/app/?setup=…" }
}
| Champ | Type | Description |
|---|---|---|
organisation | Établissement | L'établissement ouvert |
admin | object ou null | null sans responsable demandé |
admin.invited | boolean | true si le compte a été créé ; false s'il existait et a été rattaché |
admin.link | string ou null | Le lien d'activation, quand le courriel n'a pas pu partir. Aujourd'hui, MeetingHub n'en envoie pas : le lien est toujours rendu pour un compte créé. Transmettez-le vous-même, à la seule personne concernée |
Vos webhooks d'éditeur sont recopiés dans l'établissement dès son ouverture.
| Code | Corps | Cause |
|---|---|---|
400 | identifiant attendu : minuscules, chiffres et tirets, de 1 à 40 caractères | slug absent ou mal formé |
400 | nom de l’établissement requis | name absent ou vide |
400 | type inconnu — « school » ou « company » | kind invalide |
400 | sandbox attend vrai ou faux | sandbox n'est pas un booléen |
400 | adresse de l’administrateur invalide | admin fourni sans courriel lisible |
409 | un établissement porte déjà cet identifiant | slug déjà pris |
GET /api/institutions/:institutionId
L'établissement, et ce que vous pouvez y faire.
{
"institution": { "id": "6c1d3a52-…", "name": "Lycée bilingue de Bonamoussadi", "status": "active", "trialEndsAt": null },
"capabilities": ["institution.view", "partner.manage"],
"support": false
}
En pause, capabilities ne contient plus que institution.view.
L'objet Centre
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant du centre |
name | string | Son nom |
reference | string ou null | Votre code pour ce centre, unique dans l'établissement |
city | string ou null | Sa ville |
createdAt | datetime | Sa date de création |
GET /api/institutions/:institutionId/centres
Les centres de l'établissement. Exige institution.view. Réponse 200 : { "centres": [Centre] }.
POST /api/institutions/:institutionId/centres
Déclare un centre. Exige partner.manage — ou, pour l'établissement, institution.manage.
{ "name": "Campus de Kotto", "reference": "KOT", "city": "Douala"
}
| Champ | Type | Obligatoire | Règles |
|---|---|---|---|
name | string | oui | Espaces resserrés, tronqué à 120 caractères |
reference | string ou null | non | 64 caractères au plus : lettres, chiffres, point, tiret et soulignement, en commençant par une lettre ou un chiffre |
city | string | non | Tronquée à 120 caractères |
Réponse 201 : { "centre": Centre }.
| Code | Corps | Cause |
|---|---|---|
400 | nom du site requis | name absent ou vide |
400 | référence attendue : lettres, chiffres, point, tiret — 64 caractères au plus | reference mal formée |
409 | un centre de l’établissement porte déjà cette référence | reference déjà prise |
Renommer ou retirer un centre, et nommer ses directeurs, reviennent à l'administration de l'établissement.
Les clés de vos établissements
GET, POST et DELETE sous /api/institutions/:institutionId/keys : mêmes routes, mêmes corps et mêmes réponses que pour l'établissement — voir POST /api/institutions/:institutionId/keys et L'objet Clé. Avec trois différences :
- vous ne voyez et ne révoquez que les clés que vous avez créées ; une autre répond
404,clé inconnue. Elles portent votre identifiant danspartnerId; - sans accord sur les données personnelles,
attendance:readetroster:writevous sont refusées à la création :403,portée réservée aux éditeurs partenaires sous accord de données personnelles : …. L'exploitant lève cette restriction une fois l'accord de sous-traitance signé. Si l'accord vous est retiré ensuite, vos clés perdent ces deux portées dès leur requête suivante ; - le journal d'une clé,
GET …/keys/:keyId/activity, reste réservé à l'administration de l'établissement.
{ "label": "Votre logiciel — emploi du temps", "scopes": ["schedule:read", "schedule:write"]
}
La clé en clair est rendue dans key.plaintext, cette fois seulement.
POST /api/institutions/:institutionId/entries
Un lien d'entrée dans une salle de l'établissement, à ouvrir dans une iframe. Exige une clé d'éditeur et partner.manage.
{ "room": "ABCD2345", "displayName": "Awa Ngono", "host": false, "ttlMinutes": 120
}
| Champ | Type | Obligatoire | Règles |
|---|---|---|---|
room | string | oui | L'identifiant court de la salle |
displayName | string | oui | Le nom affiché, 60 caractères au plus. La personne ne peut pas le changer |
host | boolean | non | true : la personne entre comme animateur |
ttlMinutes | integer | non | De 1 à 720 ; 120 par défaut |
Réponse 201 :
{
"entry": {
"token": "eyJvIjoi….1791000000.Xk2…",
"url": "https://reunion.exemple.cm/app/?room=ABCD2345&entree=…",
"expiresAt": "2026-10-14T10:00:00.000Z"
}
}
À l'entrée, le lien tient lieu de politique d'entrée et de mot de passe. Il ne passe outre ni l'horaire du cours, ni le verrou, ni la salle d'attente, ni une exclusion. La page ne se laisse encadrer que par vos origines enregistrées (Content-Security-Policy: frame-ancestors).
| Code | Corps | Cause |
|---|---|---|
400 | nom de la personne requis | displayName absent ou vide |
400 | host attend vrai ou faux | host n'est pas un booléen |
400 | durée du lien : de 1 à 720 minutes | ttlMinutes hors bornes ou pas un entier |
404 | salle inconnue | Aucune salle de cet identifiant dans l'établissement |
409 | aucune origine d’intégration déclarée : l’exploitant doit d’abord l’enregistrer | Aucune origine enregistrée pour vous : demandez-le à l'exploitant |
L'objet Webhook d'éditeur
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant du webhook, le :webhookId des routes |
url | string | L'adresse appelée |
events | string[] | Les événements souscrits |
label | string ou null | Son nom, 120 caractères au plus |
createdAt | datetime | Sa date de création |
GET /api/partner/webhooks
Vos webhooks. Réponse 200 : { "webhooks": [Webhook], "events": [string] }. Le champ events liste tous les événements qui existent.
POST /api/partner/webhooks
{ "url": "https://api.votre-logiciel.cm/meetinghub", "events": ["attendance.closed"], "label": "Production"
}
Mêmes règles d'adresse et d'événements que pour un webhook d'établissement : voir Webhooks. Le webhook est recopié dans chacun de vos établissements, présents et à venir ; chaque appel porte l'identifiant de l'établissement dans organisationId.
Réponse 201 : { "webhook": Webhook, "notice": "…" }. Le secret de signature est dans webhook.secret, cette fois seulement.
PATCH /api/partner/webhooks/:webhookId
Change url, events ou label. Toutes les copies suivent ; ce qui est propre à un établissement — un webhook coupé après trop d'échecs — reste le sien. Réponse 200 : { "webhook": Webhook }. Un webhook inconnu répond 404, alerte inconnue.
DELETE /api/partner/webhooks/:webhookId
Supprime le webhook et toutes ses copies. Réponse 204. Un webhook inconnu répond 404, alerte inconnue.
GET /api/partner/usage
Ce que chaque établissement a consommé sur un mois calendaire, en UTC.
| Paramètre | Type | Description |
|---|---|---|
month | AAAA-MM | Le mois ; le mois en cours par défaut. Mal formé : 400, mois attendu au format AAAA-MM |
Réponse 200 :
{
"month": "2026-10",
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-11-01T00:00:00.000Z",
"organisations": [
{
"organisationId": "…",
"name": "Lycée bilingue de Bonamoussadi",
"status": "active",
"rooms": 12,
"connections": 1840,
"participants": 410,
"seconds": 2952000,
"bytes": 61200000000,
"sessionsHeld": 96
}
]
}
| Champ | Type | Description |
|---|---|---|
organisationId | uuid | L'établissement |
rooms | integer | Les salles qui ont servi |
connections | integer | Les connexions ; une personne qui se reconnecte en compte plusieurs |
participants | integer | Les personnes distinctes, comptes ou invités |
seconds | integer | Le temps de connexion cumulé |
bytes | integer | Les octets envoyés et reçus, comptés par les navigateurs |
sessionsHeld | integer | Les cours tenus |
Aucun nom, aucune adresse : ce sont des totaux.
Les anciennes adresses
Les routes /api/partner/… répondent encore, avec une clé d'éditeur seulement, comme alias dépréciés : chaque réponse porte Deprecation: true et un en-tête Link vers la route qui la remplace. Sans clé d'éditeur, elles répondent 401, clé d’éditeur partenaire requise, code auth.required.
| Ancienne adresse | Route de /api |
|---|---|
GET, POST /api/partner/organisations | GET, POST /api/institutions |
GET, POST /api/partner/organisations/:orgId/sites | GET, POST /api/institutions/:institutionId/centres |
GET, POST /api/partner/organisations/:orgId/keys | GET, POST /api/institutions/:institutionId/keys |
DELETE /api/partner/organisations/:orgId/keys/:keyId | DELETE /api/institutions/:institutionId/keys/:keyId |
POST /api/partner/organisations/:orgId/entries | POST /api/institutions/:institutionId/entries |
GET, POST /api/partner/webhooks | GET, POST /api/partner/webhooks |
PATCH, DELETE /api/partner/webhooks/:id | PATCH, DELETE /api/partner/webhooks/:webhookId |
GET /api/partner/usage | GET /api/partner/usage |
Les réponses sont celles de /api : GET /api/partner/organisations rend donc institutions, et la liste des centres centres.