Skip to content

Webhooks

Webhooks notify your server when something happens in Meetingize, so you do not have to poll. Each delivery is an HTTPS POST with a JSON event, signed with a secret only you and Meetingize know.

In the web app: Settings > Developers > Add an endpoint. Or through the API with the webhooks scope:

Terminal window
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"]}'

The response contains the signing secret (whsec_...), shown only once. URLs must use https and resolve to a public address (private and loopback addresses are refused). To test locally, expose your endpoint with a tunnel (for example ngrok or Cloudflare Tunnel).

TypeWhendata.object
transcription.completedthe diarized transcript is availablejob (+ meeting)
transcription.failedthe transcription failedjob with error
processing.completedthe AI meeting report is readyjob (+ meeting)
processing.failedthe AI report failed after a successful transcriptionjob with error
document.generateda document generation succeededjob (+ document)
document.faileda document generation failedjob with error
usage.threshold_reachedAPI usage of the month reached 50, 80 or 100 % of the wallet, or 80 or 100 % of the spend capusage_alert
ping“send a test” button or POST /webhook_endpoints/{id}/test{ "message": "..." }

Subscribe to "*" to receive every event except 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 is the hex HMAC-SHA256 of "{t}.{raw_body}" with your full secret (whsec_...) as key. To verify:

  1. read the raw request body (before any JSON parsing);
  2. recompute the HMAC and compare it in constant time with each v1= value of the header;
  3. refuse the event if t is more than 5 minutes away from now (replay protection);
  4. deduplicate on Meetingize-Event-Id (deliveries are at least once).

During a secret rotation the old secret stays valid for 24 hours and the header carries two v1= values.

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)
# Flask example
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()
# enqueue work, return fast
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();
// Keep the raw body: verification must run on the exact bytes received
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);
});
Terminal window
BODY_FILE=event.json # raw body as received
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"

The meetingize CLI also has meetingize webhooks verify.

  • Your endpoint must answer 2xx within 10 seconds. Redirects are not followed. Do the work asynchronously and answer fast.
  • Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (8 attempts in total), then marked failed.
  • After 15 consecutive failed attempts the endpoint is disabled (disabled_reason: "too_many_failures") and the owner is emailed. Re-enable it with PATCH /v1/webhook_endpoints/{id} {"enabled": true} or from the web app.
  • The delivery log (status, HTTP code, first 1 KB of your response, duration, next retry) is visible in the web app and with GET /v1/webhook_endpoints/{id}/deliveries, kept 30 days.
  • Ordering is not guaranteed. Use created and the job status, not the arrival order.

POST /v1/webhook_endpoints/{id}/rotate_secret returns a new secret. Both secrets sign deliveries for 24 hours, which gives you time to deploy the new one.