REVOIR

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_....

Ce secret n'est affiché qu'une seule fois. Copiez-le immédiatement. Sans lui, vous ne pouvez pas vérifier les signatures, et il n'est pas récupérable : il faut recréer le webhook.

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

ChampTypeDescription
eventstringToujours "extraction.completed" pour cet événement.
video.idstring (uuid)Identifiant de la réunion dans Revoir. Sert de clé de déduplication.
video.titlestringTitre de la réunion tel qu’affiché dans Revoir.
video.durationinteger | nullDurée en secondes. null si Revoir n’a pas pu la mesurer.
video.extractedAtstring (ISO 8601)Fin de l’extraction, en UTC.
downloads.jsonstring (url)Export complet en JSON. Requiert une clé d’API en Bearer.
downloads.xmlstring (url)Même document en XML. Requiert une clé d’API en Bearer.
downloads.markdownstring (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.

ChampTypeDescription
extraction.languagestring | nullNom 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.summarystringSommaire de la réunion. Plafonné à 2000 caractères.
extraction.keyTopicsstring[]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.countsobjectTotaux RÉELS du document, avant plafonnement. Comparez-les aux longueurs reçues.
extraction.truncatedobjectUn booléen par section, plus "any". true signifie qu’il manque du contenu.
Attention à 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.

ChampTypeDescription
summary2000 caractèresCoupé net, terminé par le caractère de points de suspension.
keyTopics15 éléments x 160 car.Les premiers du document sont conservés.
participants12 élémentsname 120 car., role 80 car., description 200 car.
decisions15 élémentsdescription 400 car., context 300 car., madeBy 120 car.
actionItems25 élémentsdescription 400 car., assignee 120 car., deadline 60 car.
transcriptjamais inclusLa 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 = true et counts.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 , et truncated.decisions vaut quand même true.

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).

  1. Lisez l'en-tête Revoir-Signature. Il a la forme t=<horodatage>,v1=<signature>. Découpez sur la virgule, puis sur le premier signe égal de chaque morceau.
  2. t est l'heure d'envoi en secondesdepuis l'époque Unix (pas en millisecondes).
  3. 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>.
  4. Calculez le HMAC-SHA256 de cette chaîne, avec votre secret whsec_... comme clé. Le secret est utilisé en entier, préfixe whsec_ compris, comme une chaîne UTF-8. Encodez le résultat en hexadécimal minuscule.
  5. Comparez à la valeur de v1, idéalement en temps constant. Si elles diffèrent, rejetez la requête.
Le corps brut, pas le corps reparsé.La signature porte sur les octets exacts reçus. Si votre outil transforme le JSON en objet puis le re-sérialise, l'ordre des clés et les espaces changent, et la signature ne validera jamais. Dans Express, il faut 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.
Le compteur porte sur des livraisons, pas sur des tentatives : 10 échecs consécutifs représentent 50 requêtes HTTP et au moins plusieurs heures. Un incident bref chez vous ne coupe pas le webhook.

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.

ChampTypeDescription
video.titleSujetSujet de l’activité ou de la note de réunion.
video.extractedAtDateDate de l’activité. Convertissez en heure locale au besoin.
video.durationDuréeEn secondes. Divisez par 60 pour des minutes.
extraction.summaryDescription / NoteCorps de la note de réunion.
extraction.keyTopicsÉtiquettesUne é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âchesUne tâche par élément. assignee vers le propriétaire, deadline vers l’échéance.
extraction.participants[]ContactsRapprochez name de vos contacts. Revoir ne fournit pas de courriel.
video.idChamp techniqueStockez-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.completed avec le même video.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 test valant true à la racine, plus sentAt. Filtrez dessus si vous ne voulez pas créer de fiche à partir d'un test ;
  • un video.id tout à zéro (00000000-0000-4000-8000-000000000000), qui ne correspond à aucune vraie réunion. Les liens de downloadsd'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 :

info@agenceelan.ca

← Retour à l'accueilSpécification OpenAPIConfidentialité