Aller au contenu

Webhooks

Les webhooks avertissent votre serveur quand quelque chose se produit dans Meetingize, afin que vous n’ayez pas à faire de polling. Chaque envoi est un POST HTTPS avec un événement JSON, signé avec un secret que seuls vous et Meetingize connaissez.

Dans l’application web : Paramètres > Développeurs > Ajouter un endpoint. Ou via l’API avec le scope webhooks :

Fenêtre de terminal
curl -s -X POST "$MEETINGIZE_API/webhook_endpoints" \
-H "Authorization: Bearer $MEETINGIZE_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://hooks.example.com/meetingize", "events": ["transcription.completed", "document.generated"]}'

La réponse contient le secret de signature (whsec_...), affiché une seule fois. Les URLs doivent utiliser https et pointer vers une adresse publique (les adresses privées et de loopback sont refusées). Pour tester en local, exposez votre endpoint avec un tunnel (par exemple ngrok ou Cloudflare Tunnel).

TypeQuanddata.object
transcription.completedla transcription diarisée est disponiblejob (+ meeting)
transcription.failedla transcription a échouéjob avec error
processing.completedle compte rendu IA de la réunion est prêtjob (+ meeting)
processing.failedle compte rendu IA a échoué après une transcription réussiejob avec error
document.generatedune génération de document a réussijob (+ document)
document.failedune génération de document a échouéjob avec error
usage.threshold_reachedl’usage API du mois a atteint 50, 80 ou 100 % du wallet, ou 80 ou 100 % du plafond de dépenseusage_alert
pingbouton “envoyer un test” ou POST /webhook_endpoints/{id}/test{ "message": "..." }

Abonnez-vous à "*" pour recevoir tous les événements sauf ping.

{
"id": "evt_7d1f...",
"object": "event",
"type": "transcription.completed",
"created": "2026-10-11T14:03:22Z",
"api_version": "2026-10-01",
"livemode": false,
"data": {
"object": {
"id": "job_9b0c...", "object": "job", "kind": "transcription", "status": "running",
"stage": "processing", "meeting_id": "1b9e...", "metadata": { "crm_id": "42" },
"meeting": { "id": "1b9e...", "object": "meeting", "title": "RDV client", "status": "transcribed" }
}
}
}
Content-Type: application/json
User-Agent: Meetingize-Webhooks/1.0
Meetingize-Event-Id: evt_7d1f...
Meetingize-Event-Type: transcription.completed
Meetingize-Delivery-Id: whd_...
Meetingize-Signature: t=1760191402,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 est le HMAC-SHA256 hexadécimal de "{t}.{raw_body}" avec votre secret complet (whsec_...) comme clé. Pour vérifier :

  1. lisez le corps brut de la requête (avant tout parsing JSON) ;
  2. recalculez le HMAC et comparez-le en temps constant à chaque valeur v1= de l’en-tête ;
  3. refusez l’événement si t est éloigné de plus de 5 minutes de l’heure actuelle (protection contre le rejeu) ;
  4. dédupliquez sur Meetingize-Event-Id (les envois se font au moins une fois).

Pendant une rotation de secret, l’ancien secret reste valide 24 heures et l’en-tête transporte deux valeurs v1=.

import hashlib, hmac, time
def verify_meetingize_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
items = [p.split("=", 1) for p in header.split(",") if "=" in p]
timestamps = [v for k, v in items if k == "t"]
signatures = [v for k, v in items if k == "v1"]
if not timestamps or not signatures:
return False
t = int(timestamps[0])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)
# Exemple Flask
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/meetingize")
def meetingize_webhook():
if not verify_meetingize_signature(request.get_data(), request.headers.get("Meetingize-Signature", ""),
secret="whsec_..."):
abort(400)
event = request.get_json()
# mettre le travail en file d'attente, répondre vite
return "", 204
import crypto from 'node:crypto';
import express from 'express';
function verifyMeetingizeSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = (header || '').split(',').map((p) => p.split('='));
const t = Number((parts.find(([k]) => k === 't') || [])[1]);
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || !sigs.length || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
return sigs.some((s) => s.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
const app = express();
// Conserver le corps brut : la vérification doit s'effectuer sur les octets exacts reçus
app.post('/meetingize', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyMeetingizeSignature(req.body, req.get('Meetingize-Signature'), process.env.MEETINGIZE_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(204);
});
Fenêtre de terminal
BODY_FILE=event.json # corps brut tel que reçu
HEADER='t=1760191402,v1=5257a8...'
SECRET='whsec_...'
T=$(echo "$HEADER" | tr ',' '\n' | sed -n 's/^t=//p')
EXPECTED=$( { printf '%s.' "$T"; cat "$BODY_FILE"; } | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
echo "$HEADER" | tr ',' '\n' | grep -q "^v1=$EXPECTED$" && echo "valid" || echo "INVALID"

Le CLI meetingize dispose aussi de meetingize webhooks verify.

  • Votre endpoint doit répondre 2xx dans les 10 secondes. Les redirections ne sont pas suivies. Effectuez le travail de façon asynchrone et répondez vite.
  • Les envois échoués sont retentés après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h et 24 h (8 tentatives au total), puis marqués failed.
  • Après 15 tentatives échouées consécutives, l’endpoint est désactivé (disabled_reason: "too_many_failures") et le propriétaire reçoit un e-mail. Réactivez-le avec PATCH /v1/webhook_endpoints/{id} {"enabled": true} ou depuis l’application web.
  • Le journal des envois (statut, code HTTP, premier Ko de votre réponse, durée, prochaine tentative) est visible dans l’application web et avec GET /v1/webhook_endpoints/{id}/deliveries, conservé 30 jours.
  • L’ordre n’est pas garanti. Utilisez created et le statut du job, pas l’ordre d’arrivée.

POST /v1/webhook_endpoints/{id}/rotate_secret renvoie un nouveau secret. Les deux secrets signent les envois pendant 24 heures, ce qui vous laisse le temps de déployer le nouveau.