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.
Register an endpoint
Section titled “Register an endpoint”In the web app: Settings > Developers > Add an endpoint. Or through the API with the webhooks scope:
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).
Events
Section titled “Events”| Type | When | data.object |
|---|---|---|
transcription.completed | the diarized transcript is available | job (+ meeting) |
transcription.failed | the transcription failed | job with error |
processing.completed | the AI meeting report is ready | job (+ meeting) |
processing.failed | the AI report failed after a successful transcription | job with error |
document.generated | a document generation succeeded | job (+ document) |
document.failed | a document generation failed | job with error |
usage.threshold_reached | API usage of the month reached 50, 80 or 100 % of the wallet, or 80 or 100 % of the spend cap | usage_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" } } }}Headers
Section titled “Headers”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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdVerify the signature
Section titled “Verify the signature”v1 is the hex HMAC-SHA256 of "{t}.{raw_body}" with your full secret (whsec_...) as key. To verify:
- read the raw request body (before any JSON parsing);
- recompute the HMAC and compare it in constant time with each
v1=value of the header; - refuse the event if
tis more than 5 minutes away from now (replay protection); - 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.
Python
Section titled “Python”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 examplefrom 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() # enqueue work, return fast return "", 204Node.js
Section titled “Node.js”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 receivedapp.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 (debugging)
Section titled “Shell (debugging)”BODY_FILE=event.json # raw body as receivedHEADER='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.
Delivery, retries and disabling
Section titled “Delivery, retries and disabling”- Your endpoint must answer
2xxwithin 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 withPATCH /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
createdand the job status, not the arrival order.
Rotate the secret
Section titled “Rotate the secret”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.