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.
Enregistrer un endpoint
Section intitulée « Enregistrer un endpoint »Dans l’application web : Paramètres > Développeurs > Ajouter un endpoint. Ou via l’API avec le scope
webhooks :
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).
Événements
Section intitulée « Événements »| Type | Quand | data.object |
|---|---|---|
transcription.completed | la transcription diarisée est disponible | job (+ meeting) |
transcription.failed | la transcription a échoué | job avec error |
processing.completed | le compte rendu IA de la réunion est prêt | job (+ meeting) |
processing.failed | le compte rendu IA a échoué après une transcription réussie | job avec error |
document.generated | une génération de document a réussi | job (+ document) |
document.failed | une génération de document a échoué | job avec error |
usage.threshold_reached | l’usage API du mois a atteint 50, 80 ou 100 % du wallet, ou 80 ou 100 % du plafond de dépense | usage_alert |
ping | bouton “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" } } }}En-têtes
Section intitulée « En-têtes »Content-Type: application/jsonUser-Agent: Meetingize-Webhooks/1.0Meetingize-Event-Id: evt_7d1f...Meetingize-Event-Type: transcription.completedMeetingize-Delivery-Id: whd_...Meetingize-Signature: t=1760191402,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdVérifier la signature
Section intitulée « Vérifier la signature »v1 est le HMAC-SHA256 hexadécimal de "{t}.{raw_body}" avec votre secret complet (whsec_...) comme clé. Pour
vérifier :
- lisez le corps brut de la requête (avant tout parsing JSON) ;
- recalculez le HMAC et comparez-le en temps constant à chaque valeur
v1=de l’en-tête ; - refusez l’événement si
test éloigné de plus de 5 minutes de l’heure actuelle (protection contre le rejeu) ; - 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 Flaskfrom flask import Flask, request, abortapp = 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 "", 204import 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çusapp.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);});Shell (débogage)
Section intitulée « Shell (débogage) »BODY_FILE=event.json # corps brut tel que reçuHEADER='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.
Envoi, tentatives et désactivation
Section intitulée « Envoi, tentatives et désactivation »- Votre endpoint doit répondre
2xxdans 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 avecPATCH /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
createdet le statut du job, pas l’ordre d’arrivée.
Faire tourner le secret
Section intitulée « Faire tourner le secret »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.