Documentation intégrateur
Webhooks : l'événement extraction.completed
Dernière mise à jour : 30 juillet 2026
Quand Revoir a fini d'analyser une réunion, il envoie une requête POST à l'adresse de votre choix, avec le sommaire, les sujets, les décisions et les tâches directement dans le corps JSON. Pas de sondage, pas d'attente.
Cette page décrit exactement ce que vous recevez et comment le vérifier. Tout ce qui suit a été vérifié contre le code qui envoie réellement l'événement.
Créer un webhook
Dans Revoir : Réglages, onglet Webhooks. Vous donnez une URL en https et cochez les événements. Revoir affiche alors un secret de la forme whsec_....
Le même écran est disponible par l'API : POST /api/v1/webhooks avec { "url": "...", "events": ["extraction.completed"] }. Le secret est dans la réponse de création, et nulle part ailleurs.
1. Quand l'événement part
extraction.completedpart une fois par réunion, à la toute fin du traitement : après la transcription, l'extraction du document et l'indexation, au moment où la réunion passe à l'état ready dans Revoir.
Il existe aussi extraction.failed, envoyé quand le traitement échoue. Sa charge utile est volontairement minimale : event, video.id et error.message. Il n'y a pas de document à joindre dans ce cas.
Un même événement peut arriver plusieurs fois si votre serveur n'a pas répondu du premier coup (voir la section relances). Utilisez video.id comme clé pour ne pas créer deux fois la même fiche.
2. La charge utile exacte
Voici un corps réel, tel qu'il arrive sur le fil :
{
"event": "extraction.completed",
"video": {
"id": "3f2a9c10-5b6d-4e7f-8a91-0b2c3d4e5f60",
"title": "Appel Voltix - cadrage du pilote",
"duration": 2530,
"extractedAt": "2026-07-27T18:42:11.204Z"
},
"downloads": {
"xml": "https://api.revoir.agenceelan.ca/api/v1/videos/3f2a9c10-5b6d-4e7f-8a91-0b2c3d4e5f60/export.xml",
"markdown": "https://api.revoir.agenceelan.ca/api/v1/videos/3f2a9c10-5b6d-4e7f-8a91-0b2c3d4e5f60/export.md",
"json": "https://api.revoir.agenceelan.ca/api/v1/videos/3f2a9c10-5b6d-4e7f-8a91-0b2c3d4e5f60/export.json"
},
"extraction": {
"language": "French",
"summary": "Appel de cadrage avec Voltix sur l'intégration Zoho CRM. Pascal confirme vouloir recevoir le sommaire, les décisions et les tâches directement dans le CRM, sans intervention humaine.",
"keyTopics": [
"Intégration CRM",
"Webhooks",
"Projet pilote",
"Échéancier"
],
"participants": [
{
"name": "Pascal Bastien",
"role": "Président",
"description": "Dirige Voltix."
},
{
"name": "Margot Lévesque",
"role": "Technique",
"description": "Câble le Zoho Flow."
},
{
"name": "Alex Mercier",
"role": "Fondateur",
"description": "Présente Revoir."
}
],
"decisions": [
{
"timestamp": "00:21:40",
"madeBy": "Pascal Bastien",
"description": "Voltix finance le projet pilote de Revoir pour l'automne.",
"context": "Décision prise après la démonstration de l'extraction automatique."
},
{
"timestamp": "00:29:15",
"madeBy": "Margot Lévesque",
"description": "Le Zoho Flow écoutera extraction.completed plutôt que de sonder l'API.",
"context": "Évite une deuxième requête authentifiée à chaque réunion."
}
],
"actionItems": [
{
"assignee": "Margot Lévesque",
"deadline": "2026-08-07",
"timestamp": "00:38:05",
"description": "Câbler le Zoho Flow sur le webhook et vérifier la signature HMAC."
},
{
"assignee": "Alex Mercier",
"deadline": null,
"timestamp": "00:41:20",
"description": "Transmettre la clé d'API et le secret du webhook à Voltix."
}
],
"counts": {
"segments": 12,
"keyTopics": 4,
"participants": 3,
"decisions": 2,
"actionItems": 2
},
"truncated": {
"summary": false,
"keyTopics": false,
"participants": false,
"decisions": false,
"actionItems": false,
"any": false
}
}
}Champs de premier niveau
| Champ | Type | Description |
|---|---|---|
| event | string | Toujours "extraction.completed" pour cet événement. |
| video.id | string (uuid) | Identifiant de la réunion dans Revoir. Sert de clé de déduplication. |
| video.title | string | Titre de la réunion tel qu’affiché dans Revoir. |
| video.duration | integer | null | Durée en secondes. null si Revoir n’a pas pu la mesurer. |
| video.extractedAt | string (ISO 8601) | Fin de l’extraction, en UTC. |
| downloads.json | string (url) | Export complet en JSON. Requiert une clé d’API en Bearer. |
| downloads.xml | string (url) | Même document en XML. Requiert une clé d’API en Bearer. |
| downloads.markdown | string (url) | Même document en Markdown. Requiert une clé d’API en Bearer. |
Le bloc extraction
C'est le contenu de la réunion, en JSON natif. Pas de XML échappé dans une chaîne, pas de deuxième requête à faire pour l'essentiel.
| Champ | Type | Description |
|---|---|---|
| extraction.language | string | null | Nom de la langue détectée, en anglais : "French", "English". Ce n’est PAS un code ISO, et la valeur n’est pas normalisée. |
| extraction.summary | string | Sommaire de la réunion. Plafonné à 2000 caractères. |
| extraction.keyTopics | string[] | Sujets principaux. Au plus 15, chacun au plus 160 caractères. |
| extraction.participants[] | object[] | name (string), role (string | null), description (string). Au plus 12. |
| extraction.decisions[] | object[] | timestamp, madeBy, description, context. Au plus 15. |
| extraction.actionItems[] | object[] | assignee, deadline (string | null), timestamp, description. Au plus 25. |
| extraction.counts | object | Totaux RÉELS du document, avant plafonnement. Comparez-les aux longueurs reçues. |
| extraction.truncated | object | Un booléen par section, plus "any". true signifie qu’il manque du contenu. |
language.C'est le nom de la langue tel que produit par l'analyse, en anglais et sans normalisation : vous recevrez "French", pas "fr". Ne construisez pas de branche sur un code ISO. La valeur peut aussi être null.Le bloc extraction peut être absent si Revoir n'a pas pu relire le document. Les champs video et downloads, eux, sont toujours là. Programmez votre flux en conséquence : testez la présence du bloc avant de le lire.
3. Plafonds et troncature
La charge utile est bornée pour qu'une réunion de 90 minutes ne produise pas un corps démesuré, qui serait renvoyé tel quel à chaque relance. Un corps typique fait entre 2 et 8 kilo-octets. La borne haute théorique, tous champs saturés, est sous 80 kilo-octets.
| Champ | Type | Description |
|---|---|---|
| summary | 2000 caractères | Coupé net, terminé par le caractère de points de suspension. |
| keyTopics | 15 éléments x 160 car. | Les premiers du document sont conservés. |
| participants | 12 éléments | name 120 car., role 80 car., description 200 car. |
| decisions | 15 éléments | description 400 car., context 300 car., madeBy 120 car. |
| actionItems | 25 éléments | description 400 car., assignee 120 car., deadline 60 car. |
| transcript | jamais inclus | La transcription mot à mot n’est jamais dans la charge utile. Passez par l’export. |
Quand un plafond mord, Revoir vous le dit : le booléen correspondant dans extraction.truncated passe à true. Le drapeau couvre les deux formes de coupe :
- liste écourtée : une réunion avec 30 décisions vous en envoie 15, avec
truncated.decisions = trueetcounts.decisions = 30; - texte coupé dans un élément conservé : 3 décisions dont une de 900 caractères vous arrivent toutes les 3, mais la longue est ramenée à 400 caractères suivis de
…, ettruncated.decisionsvaut quand mêmetrue.
Autrement dit : countsne suffit pas à savoir s'il vous manque quelque chose, c'est truncatedqui fait foi. La longueur d'une liste reçue vaut toujours min(plafond, counts).
Si truncated.any vaut true et que vous avez besoin de la totalité, allez la chercher avec downloads.json(section 5). C'est exactement à ça que sert ce lien.
4. Vérifier la signature
Voici les en-têtes envoyés avec chaque livraison :
POST /votre-endpoint HTTP/1.1 Content-Type: application/json User-Agent: Revoir-Webhook/1.0 Revoir-Event: extraction.completed Revoir-Signature: t=1785437495,v1=a5ae5564fa8b31b1373159c416f17016d680ffbfca460afd71784e05564894c1
L'algorithme, en clair
Reproductible dans n'importe quel langage. Le nom exact de l'en-tête est Revoir-Signature (les en-têtes HTTP sont insensibles à la casse).
- Lisez l'en-tête
Revoir-Signature. Il a la formet=<horodatage>,v1=<signature>. Découpez sur la virgule, puis sur le premier signe égal de chaque morceau. test l'heure d'envoi en secondesdepuis l'époque Unix (pas en millisecondes).- Composez la chaîne signée en concaténant : la valeur de t, un point, puis le corps brut de la requête. Soit littéralement
<t>.<corps>. - Calculez le HMAC-SHA256 de cette chaîne, avec votre secret
whsec_...comme clé. Le secret est utilisé en entier, préfixewhsec_compris, comme une chaîne UTF-8. Encodez le résultat en hexadécimal minuscule. - Comparez à la valeur de
v1, idéalement en temps constant. Si elles diffèrent, rejetez la requête.
express.raw() et non express.json().Exemple complet en Node
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.REVOIR_WEBHOOK_SECRET; // whsec_...
// Important: le corps BRUT est nécessaire. Un corps déjà transformé en objet
// puis re-sérialisé ne produit pas les mêmes octets, et la signature échoue.
app.post('/revoir', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
const header = req.get('Revoir-Signature') || '';
// 1. Découper l'en-tête "t=<epoch>,v1=<hex>"
const parts = new Map();
for (const piece of header.split(',')) {
const i = piece.indexOf('=');
if (i !== -1) parts.set(piece.slice(0, i).trim(), piece.slice(i + 1).trim());
}
const t = parts.get('t');
const v1 = parts.get('v1');
if (!t || !v1) return res.status(400).send('signature absente');
// 2. Rejeter un horodatage trop vieux (protection contre le rejeu).
// 5 minutes est une tolérance raisonnable; c'est votre choix, pas le nôtre.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) {
return res.status(400).send('horodatage hors tolérance');
}
// 3. Recomposer la chaîne signée, puis le HMAC.
const signedPayload = t + '.' + rawBody;
const expected = crypto.createHmac('sha256', SECRET).update(signedPayload).digest('hex');
// 4. Comparer en temps constant.
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(v1, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('signature invalide');
}
const event = JSON.parse(rawBody);
// ... votre traitement ...
// 5. Répondre 2xx rapidement. Le traitement long se fait après la réponse.
res.status(200).send('ok');
});Ce que Revoir ne fait pas
Revoir ne vérifie pasl'âge de l'horodatage à votre place, et n'impose aucune fenêtre de tolérance. Le rejet d'un événement rejoué est votre décision. La valeur de 5 minutes dans l'exemple ci-dessus est une suggestion, pas une règle de Revoir.
Exception Slack.Si l'URL du webhook est sur hooks.slack.com, Revoir remplace le corps par un message Slack de la forme { "text": "..." } et n'envoie aucune signature. C'est un raccourci pour brancher Slack sans intermédiaire. Pour toute autre destination, le corps est la charge utile décrite ici et la signature est toujours présente.
5. Aller chercher le document complet
Les trois liens de downloads pointent vers l'API v1 et exigent une clé d'API en Bearer. Ce ne sont pas des liens publics : ouverts dans un navigateur sans en-tête, ils répondent 401.
La clé se génère dans Revoir : Réglages, onglet Clés API. Elle a la forme revoir_live_<hex>et, comme le secret du webhook, elle n'est affichée qu'une fois.
curl -H "Authorization: Bearer revoir_live_VOTRE_CLE" \ "https://api.revoir.agenceelan.ca/api/v1/videos/3f2a9c10-5b6d-4e7f-8a91-0b2c3d4e5f60/export.json?segments=false"
Pour un outil sans code, utilisez export.json: c'est le même document que le XML, mais directement exploitable, sans analyseur.
{
"data": {
"video": { "id": "...", "title": "...", "duration": 2530, "extractedAt": "..." },
"metadata": { "sourceFile": "...", "duration": "00:42:10", "detectedLanguage": "French", ... },
"summary": { "overallSummary": "...", "keyTopics": [...], "participants": [...] },
"decisions": [ { "timestamp": "...", "madeBy": "...", "description": "...", "context": "..." } ],
"actionItems": [ { "assignee": "...", "deadline": null, "timestamp": "...", "description": "..." } ],
"segments": [ { "id": "...", "start": "...", "end": "...", "transcript": [...], ... } ],
"counts": { "segments": 12, "decisions": 2, "actionItems": 2 },
"segmentsIncluded": true
}
}Deux paramètres facultatifs : ?segments=false retire les segments et leur transcription (charge beaucoup plus légère, les décisions et les tâches restent), et ?visuals=falseretire la description de ce qui était affiché à l'écran. Le champ counts donne toujours les totaux du document, même quand les segments sont retirés.
Les exports ne sont disponibles que lorsque le traitement est terminé. Sinon, la réponse est un 422 avec l'état courant dans error.details.status. Comme le webhook part précisément à la fin du traitement, ce cas ne devrait pas se produire dans un flux déclenché par l'événement.
Les appels API sont limités par heure selon le forfait. La spécification complète est publiée en OpenAPI 3.1 : api.revoir.agenceelan.ca/api/docs.
6. Relances et désactivation automatique
Votre serveur doit répondre un statut 2xx. Répondez vite : Revoir abandonne une tentative après 10 secondes. Faites votre traitement long après avoir répondu.
- Toute réponse non 2xx, ou aucune réponse dans le délai, compte comme un échec.
- Une livraison est tentée jusqu'à 5 fois, en repli exponentiel à partir de 60 secondes : environ 1, puis 2, puis 4, puis 8 minutes entre les tentatives. La fenêtre totale est d'environ 15 minutes.
- Si les 5 tentatives échouent, la livraison est comptée comme un échec, une seule fois.
- Après 10 livraisons en échec consécutives, le webhook est désactivé automatiquement. Il cesse de recevoir des événements jusqu'à réactivation manuelle dans Revoir.
- Une seule livraison réussie remet le compteur à zéro.
L'historique des livraisons, avec le statut renvoyé par votre serveur, est visible dans Revoir sous Réglages, onglet Webhooks. C'est le premier endroit à regarder quand quelque chose n'arrive pas.
7. Quels champs mapper vers quoi
Correspondances usuelles vers un CRM. Les noms de destination varient selon le module (Réunion, Activité, Note, Tâche) : adaptez au vôtre.
| Champ | Type | Description |
|---|---|---|
| video.title | Sujet | Sujet de l’activité ou de la note de réunion. |
| video.extractedAt | Date | Date de l’activité. Convertissez en heure locale au besoin. |
| video.duration | Durée | En secondes. Divisez par 60 pour des minutes. |
| extraction.summary | Description / Note | Corps de la note de réunion. |
| extraction.keyTopics | Étiquettes | Une étiquette par sujet, ou une liste à puces dans la note. |
| extraction.decisions[] | Note ou champ dédié | description est la phrase utile; context explique le pourquoi. |
| extraction.actionItems[] | Tâches | Une tâche par élément. assignee vers le propriétaire, deadline vers l’échéance. |
| extraction.participants[] | Contacts | Rapprochez name de vos contacts. Revoir ne fournit pas de courriel. |
| video.id | Champ technique | Stockez-le pour éviter de créer deux fois la même note si une relance survient. |
Pour créer une tâche par élément dans un outil sans code, itérez sur extraction.actionItems. Attention : deadline peut valoir null, et timestamp est une position dans la vidéo (00:38:05), pas une date.
8. Ce que Revoir ne fournit pas
Pour éviter toute mauvaise surprise au moment du câblage, voici les limites réelles d'aujourd'hui.
- Revoir ne génère pas de brouillon de courriel de suivi. L'extraction produit un sommaire, des décisions et des tâches. Le courriel de repli se compose de votre côté, à partir de ces données structurées. Aucun champ de la charge utile ne contient de courriel rédigé.
- Aucune adresse courriel de participant.
participants[].nameest le nom tel qu'entendu ou lu dans la réunion. Le rapprochement avec vos contacts est à votre charge, et les noms peuvent être approximatifs. - Pas de transcription dans le webhook. Elle est disponible uniquement par les exports.
- Pas d'IP fixe pour la liste blanche. Si votre pare-feu exige une origine fixe, écrivez-nous avant de câbler.
- Pas d'événement de modification. Si une réunion est retraitée, vous recevez un nouvel
extraction.completedavec le mêmevideo.id. À vous de décider si vous mettez à jour la fiche existante ou si vous en créez une autre.
9. Tester avant la première vraie réunion
Vous n'avez pas besoin d'attendre qu'une réunion se termine pour câbler votre flux.
Depuis Revoir. Réglages, onglet Webhooks, bouton Testersur la ligne du webhook. L'envoi de test a exactement la même structure qu'un vrai événement, signée avec votre secret, avec deux différences volontaires :
- un champ
testvalanttrueà la racine, plussentAt. Filtrez dessus si vous ne voulez pas créer de fiche à partir d'un test ; - un
video.idtout à zéro (00000000-0000-4000-8000-000000000000), qui ne correspond à aucune vraie réunion. Les liens dedownloadsd'un envoi de test répondent donc 404 : c'est normal.
Envoi personnalisé.Nous pouvons aussi pousser une charge utile vers l'URL de votre choix, y compris un service de capture comme webhook.site, dans deux variantes : une réunion normale, et une réunion volumineuse où les plafonds ont mordu, pour tester votre traitement des drapeaux truncated. Demandez-le nous.
Contact
Une question sur cette page, un champ qui manque, un comportement qui ne correspond pas à ce qui est décrit ici :