Un webhook est une adresse de votre système que MeetingHub appelle quand un événement se produit dans un établissement. Il se pose de deux façons :
- par l'établissement, avec la session d'un administrateur, sous
/api/institutions/:institutionId/webhooks— la capacitéinstitution.manage, voir Conventions. Une clé d'établissement n'y a pas accès. Ces routes sont décrites ici ; - par un éditeur partenaire, avec sa clé
mhp_…, sous/api/partner/webhooks: posé une fois, le webhook vaut pour tous ses établissements. Voir Éditeurs partenaires.
Dans les deux cas, l'appel envoyé à votre serveur, sa signature et ses réessais sont les mêmes : ils sont décrits à la fin de cette page.
L'objet Webhook
{
"id": "5a74f72e-81af-402c-a2af-a5ebd35b7bba",
"url": "https://ent.mon-lycee.fr/hooks/meetinghub",
"events": ["attendance.closed"],
"label": "ENT — émargement",
"enabled": true,
"disabledReason": null,
"consecutiveFailures": 0,
"lastSuccessAt": "2026-10-14T10:30:05.000Z",
"lastFailureAt": null,
"createdAt": "2026-09-02T08:00:00.000Z",
"partnerWebhookId": null
}
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant du webhook |
url | string | L'adresse appelée, sous sa forme normalisée |
events | string[] | Les événements souscrits, sans doublon. Vide : aucun. Voir Événements |
label | string ou null | Un nom libre |
enabled | boolean | false si le webhook a été coupé, par vous ou par le serveur |
disabledReason | string ou null | Pourquoi le serveur l'a coupé ; null s'il est actif ou si c'est vous qui l'avez coupé |
consecutiveFailures | integer | Le nombre de tentatives échouées d'affilée, tous événements confondus. Remis à 0 par une livraison réussie ou une réactivation |
lastSuccessAt | datetime ou null | La dernière livraison réussie |
lastFailureAt | datetime ou null | La dernière tentative échouée |
createdAt | datetime | La date de création |
partnerWebhookId | uuid ou null | Pour la copie d'un webhook posé par l'éditeur partenaire de l'établissement : l'identifiant de ce webhook chez l'éditeur ; null sinon |
Le secret de signature n'est jamais rendu, sauf une fois, à la création.
L'objet Livraison
Une livraison est l'envoi d'un événement à un webhook, avec ses tentatives.
| Champ | Type | Description |
|---|---|---|
id | uuid | Identifiant de la livraison. C'est la valeur de l'en-tête x-meetinghub-delivery |
webhookId | uuid | Le webhook destinataire |
event | string | L'événement |
payload | string | Le corps exact envoyé, en JSON sérialisé. Il est figé à l'émission et identique à chaque tentative |
status | string | pending : en attente d'une tentative ; delivered : livrée ; failed : abandonnée |
attempts | integer | Le nombre de tentatives faites |
nextAttemptAt | datetime | La prochaine tentative prévue, pour une livraison pending |
lastAttemptAt | datetime ou null | La dernière tentative ; null avant la première |
responseStatus | integer ou null | Le code HTTP rendu par votre serveur à la dernière tentative ; null s'il n'a pas répondu. 403 pour une adresse refusée par MeetingHub |
error | string ou null | Pour un échec : les 500 premiers caractères du corps de votre réponse, ou la cause de l'échec ; null pour une réussite |
createdAt | datetime | Le moment de l'émission |
Événements
| Événement | Émis quand |
|---|---|
attendance.closed | Une feuille de présence vient d'être arrêtée, automatiquement ou à la main |
session.cancelled | Un cours est annulé un par un : depuis MeetingHub, ou par DELETE /api/institutions/:institutionId/lessons/:lessonId. Une annulation dans un dépôt d'emploi du temps, ou par sa ref, ne l'émet pas |
roster.imported | Une liste d'inscrits vient d'être appliquée, par l'API ou par un import de fichier dans MeetingHub. Un essai à blanc ne l'émet pas |
GET /api/institutions/:institutionId/webhooks
Liste tous les webhooks de l'établissement, coupés compris, et les copies des webhooks de son éditeur partenaire.
Réponse 200 :
{
"webhooks": [ { "id": "5a74f72e-…", "url": "https://ent.mon-lycee.fr/hooks/meetinghub", "…": "…" } ],
"events": ["attendance.closed", "session.cancelled", "roster.imported"]
}
| Champ | Type | Description |
|---|---|---|
webhooks | Webhook[] | Les webhooks, du plus ancien au plus récent. Voir L'objet Webhook |
events | string[] | Tous les événements qui existent |
POST /api/institutions/:institutionId/webhooks
Crée un webhook et rend son secret, une seule fois.
{
"url": "https://ent.mon-lycee.fr/hooks/meetinghub",
"events": ["attendance.closed"],
"label": "ENT — émargement"
}
| Champ | Type | Obligatoire | Règles |
|---|---|---|---|
url | string | oui | Voir Adresses acceptées |
events | string[] | oui | Peut être vide. Chaque valeur doit être un événement existant ; les doublons sont retirés |
label | string | non | Espaces de bord retirés. Vide ou absent : null |
Réponse 201 :
{
"webhook": {
"id": "5a74f72e-81af-402c-a2af-a5ebd35b7bba",
"url": "https://ent.mon-lycee.fr/hooks/meetinghub",
"events": ["attendance.closed"],
"label": "ENT — émargement",
"enabled": true,
"disabledReason": null,
"consecutiveFailures": 0,
"lastSuccessAt": null,
"lastFailureAt": null,
"createdAt": "2026-09-02T08:00:00.000Z",
"partnerWebhookId": null,
"secret": "whsec_Lc68se…"
},
"notice": "Ce secret ne sera plus affiché. Copiez-le maintenant."
}
| Champ | Type | Description |
|---|---|---|
webhook | Webhook | Le webhook créé, avec en plus le champ secret |
webhook.secret | string | Le secret de signature : whsec_ suivi de 43 caractères. Il n'est plus jamais rendu |
notice | string | Un rappel destiné à la personne qui crée le webhook |
| Code | Corps | Cause |
|---|---|---|
400 | adresse illisible | url absente ou qui n'est pas une adresse |
400 | seul https est accepté | url n'est pas en https |
400 | pas d’identifiants dans l’adresse | url contient un nom d'utilisateur ou un mot de passe |
400 | adresse interne refusée | url vise une adresse interne |
400 | adresse trop longue | url dépasse 2 000 caractères |
400 | events attend une liste | events absent ou pas un tableau |
400 | événement inconnu : … — parmi … | Une valeur de events n'est pas un événement |
Ces refus portent le code invalid.
Adresses acceptées
| Règle | Détail |
|---|---|
| Protocole | https seulement |
| Identifiants | Aucun nom d'utilisateur ni mot de passe dans l'adresse |
| Noms refusés | localhost, ip6-localhost, et tout nom qui finit par .local, .localhost, .internal, .intranet ou .home.arpa |
| Adresses IP refusées | Toute adresse privée, locale ou réservée, en IPv4 comme en IPv6, y compris une IPv4 écrite sous une forme IPv6 |
| Longueur | 2 000 caractères au plus |
La vérification est refaite à CHAQUE appel, sur les adresses obtenues en résolvant le nom : toutes doivent être publiques, et la connexion est faite à l'une d'elles, et à elle seule. Un nom qui résout vers une adresse interne au moment de l'appel est refusé, même s'il était public à la création du webhook.
PATCH /api/institutions/:institutionId/webhooks/:webhookId
Modifie un webhook. Seuls les champs présents sont modifiés.
| Paramètre de chemin | Type | Description |
|---|---|---|
webhookId | uuid | L'identifiant du webhook |
| Champ | Type | Obligatoire | Règles |
|---|---|---|---|
url | string | non | Mêmes règles qu'à la création |
events | string[] | non | Mêmes règles qu'à la création ; remplace la liste entière |
label | string ou null | non | Vide, null ou autre chose qu'une chaîne : null |
enabled | boolean | non | false coupe le webhook ; true le réactive et remet disabledReason à null et consecutiveFailures à 0 |
Une nouvelle url s'applique aussi aux livraisons encore en attente. Un webhook coupé garde ses livraisons en attente ; il les reprend quand on le réactive.
Réponse 200 :
{ "webhook": { "id": "5a74f72e-…", "enabled": true, "…": "…" }
}
| Champ | Type | Description |
|---|---|---|
webhook | Webhook | Le webhook modifié, sans son secret |
| Code | Corps | Cause |
|---|---|---|
400 | mêmes messages qu'à la création | url ou events invalide |
400 | enabled attend un booléen | enabled n'est pas true ou false |
404 | alerte inconnue | Aucun webhook de cet identifiant dans l'établissement, ou webhookId mal formé |
DELETE /api/institutions/:institutionId/webhooks/:webhookId
Supprime un webhook et tout son historique de livraisons.
| Paramètre de chemin | Type | Description |
|---|---|---|
webhookId | uuid | L'identifiant du webhook |
Réponse 204, sans corps.
| Code | Corps | Cause |
|---|---|---|
404 | alerte inconnue | Aucun webhook de cet identifiant dans l'établissement, ou webhookId mal formé |
GET /api/institutions/:institutionId/webhooks/:webhookId/deliveries
Les livraisons d'un webhook, de la plus récente à la plus ancienne. C'est aussi ainsi que l'établissement suit les livraisons d'un webhook de son éditeur partenaire : par sa copie.
| Paramètre de chemin | Type | Description |
|---|---|---|
webhookId | uuid | L'identifiant du webhook |
| Paramètre de requête | Type | Défaut | Bornes | Description |
|---|---|---|---|---|
limit | integer | 50 | 1 à 200 | Nombre de livraisons rendues. 0 ou une valeur illisible valent 50 |
Réponse 200 :
{
"deliveries": [
{
"id": "c1d2e3f4-…",
"webhookId": "5a74f72e-…",
"event": "attendance.closed",
"payload": "{\"event\":\"attendance.closed\",\"id\":\"evt_…\",…}",
"status": "delivered",
"attempts": 1,
"nextAttemptAt": "2026-10-14T10:30:05.000Z",
"lastAttemptAt": "2026-10-14T10:30:05.000Z",
"responseStatus": 200,
"error": null,
"createdAt": "2026-10-14T10:30:04.000Z"
}
]
}
| Champ | Type | Description |
|---|---|---|
deliveries | Livraison[] | Voir L'objet Livraison |
| Code | Corps | Cause |
|---|---|---|
404 | alerte inconnue | Aucun webhook de cet identifiant dans l'établissement, ou webhookId mal formé |
L'appel envoyé à votre serveur
MeetingHub fait un POST sur l'adresse du webhook.
| En-tête | Valeur |
|---|---|
content-type | application/json |
x-meetinghub-signature | t= suivi de l'heure de la tentative en secondes Unix, ,v1= puis la signature en hexadécimal. Voir Signature |
x-meetinghub-event | Le nom de l'événement |
x-meetinghub-delivery | L'identifiant de la livraison, identique à chaque tentative : il sert à dédoublonner |
user-agent | MeetingHub-Webhooks/1 |
Le corps :
{
"event": "attendance.closed",
"id": "evt_9xK2pQ7rLm3sT8vW",
"occurredAt": "2026-10-14T10:30:04.118Z",
"organisationId": "6c1d3a52-7a0e-4f0b-9c55-0a54c3e1f2b8",
"data": { "sessionId": "9214f192-…", "…": "…" }
}
| Champ | Type | Description |
|---|---|---|
event | string | Le nom de l'événement |
id | string | L'identifiant de l'événement : evt_ suivi de 16 caractères. Tous les webhooks qui reçoivent un même événement reçoivent le même id |
occurredAt | datetime | Le moment de l'émission |
organisationId | uuid | L'établissement concerné : le :institutionId des routes de /api |
data | object | Les données de l'événement, décrites ci-dessous |
attendance.closed
Champ de data | Type | Description |
|---|---|---|
sessionId | uuid | Le cours. C'est le :lessonId à passer à GET /api/institutions/:institutionId/lessons/:lessonId/attendance |
roomId | uuid | La salle |
groupId | uuid ou null | La classe attendue |
sheetId | uuid | La feuille arrêtée |
roomName | string | Le nom de la salle, tel que recopié dans la feuille |
groupName | string ou null | Le nom de la classe, tel que recopié dans la feuille |
title | string ou null | L'intitulé du cours |
startsAt | datetime | L'heure de début prévue |
durationMinutes | integer | La durée prévue |
expectedCount | integer | L'effectif attendu |
presentCount | integer | Le nombre de personnes venues |
closedAt | datetime | Le moment de l'arrêt |
signature | string ou null | Le code de vérification de la feuille |
Le corps ne contient pas la feuille elle-même : récupérez-la avec une clé de portée attendance:read. Un cours qui n'a rien à relever n'a pas de feuille, et n'émet pas cet événement.
session.cancelled
Champ de data | Type | Description |
|---|---|---|
sessionId | uuid | Le cours annulé — son :lessonId |
roomId | uuid | Sa salle |
groupId | uuid ou null | Sa classe |
title | string ou null | Son intitulé |
startsAt | datetime | Son heure de début prévue |
endsAt | datetime | Son heure de fin prévue |
cancelledAt | datetime | Le moment de l'annulation |
roster.imported
Champ de data | Type | Description |
|---|---|---|
groupId | uuid | La classe |
groupName | string | Son nom |
read | integer | Le nombre d'entrées de la liste |
accountsCreated | integer | Les comptes créés |
addedToGroup | integer | Les personnes ajoutées à la classe |
rejected | integer | Le nombre d'entrées refusées |
Il ne contient ni courriel, ni téléphone, ni lien d'activation.
Signature
v1 est un HMAC-SHA256, en hexadécimal minuscule, calculé avec le secret du webhook sur la chaîne suivante :
<t>.<corps brut>
où t est la valeur de t= dans l'en-tête, et le corps brut est la suite exacte d'octets reçue, avant toute analyse du JSON. Le secret s'utilise tel qu'il a été rendu à la création, préfixe whsec_ compris.
La signature est recalculée à chaque tentative : t est l'heure de la tentative, pas celle de l'événement. Refusez un appel dont t s'écarte de plus de 300 secondes de votre horloge : c'est ce qui empêche de rejouer un appel intercepté. Comparez les signatures à temps constant.
Livraison et réessais
| Paramètre | Valeur |
|---|---|
| Délai de réponse accordé | 10 secondes par tentative |
| Réponse acceptée | Tout code 2xx |
| Corps de votre réponse | Lu sur 8 Ko au plus ; les 500 premiers caractères d'une réponse d'échec sont gardés dans error |
| Redirections | Jamais suivies : une réponse 3xx est un échec, sans réessai |
| Réessayé | Aucune réponse, 408, 429, et tout code 5xx |
| Abandonné sans réessai | Tout autre code 4xx, les 3xx, et une adresse refusée par MeetingHub — adresse interne, ou nom qui ne se résout pas |
| Tentatives au plus | 6 : une première, puis 5 réessais |
| Délais entre les tentatives | 2, 4, 8, 16 puis 32 secondes, chacun allongé d'au plus 25 % au hasard |
| Relève de la file | Toutes les 5 secondes, par lots de 20 livraisons, envoyées en parallèle |
| Abandon | Environ une à deux minutes après l'émission, si aucune tentative n'a réussi |
| Coupure du webhook | Après 20 tentatives échouées d'affilée, tous événements confondus ; le motif est écrit dans disabledReason |
Deux conséquences à prévoir de votre côté :
- Au moins une fois. Si votre réponse se perd, la même livraison est renvoyée. Dédoublonnez sur
x-meetinghub-delivery. - Sans ordre garanti. Les livraisons partent en parallèle : ne supposez pas qu'elles arrivent dans l'ordre des événements.
Une livraison abandonnée reste consultable avec GET /api/institutions/:institutionId/webhooks/:webhookId/deliveries ; elle n'est pas renvoyée d'elle-même.