Core concepts
Webhooks
Signed callbacks when a meeting starts, finishes, or is ready to read.
A webhook is an HTTPS endpoint of yours that we POST to when something happens. It replaces polling: the notetaker can sit in an hour-long call, and you hear about it once, when the notes exist.
Events
| Event | Fires when |
|---|---|
meeting.started | The notetaker has joined the call |
meeting.ended | The call finished; processing has begun |
meeting.ready | Transcript and summary are available |
meeting.failed | The notetaker could not join, or processing failed |
calendar.connected | Someone finished connecting a calendar |
calendar.disconnected | A calendar was disconnected or its access was revoked |
The payload
https://your-app.example.com/hooks/nabrahEvery delivery carries the event name, a timestamp, and the affected record.
{
"id": "b7d41e02-5c93-4f18-a6d2-0e8c3b71f944",
"type": "meeting.ready",
"createdAt": "2026-08-12T10:02:14.000Z",
"data": {
"runId": "9f8c2e10-4b71-4d2a-8e93-1c7f5a604b18",
"reference": "NB-3F2A-9C1D-7E5B",
"meetingId": "3f2a9c1d-7e5b-4a80-9c11-2d90b8e4c177",
"title": "Weekly product sync",
"durationSeconds": 2740,
"language": "en"
}
}{ "received": true }Respond 2xx within ten seconds. Anything else is a failure, and we retry with exponential backoff — six attempts over about a minute, waiting 2s, 4s, 8s, 16s and 32s between them. A receiver that is down longer than that will miss the delivery, so read GET /webhooks/{id}/deliveries to catch up rather than relying on the retry.
An endpoint that keeps failing is switched off
After ten consecutive failures we stop sending to it and set status: "disabled", with disabledReason saying what the last attempt said. Nothing is delivered again until you re-enable it:
curl -X PATCH https://api-notetaker.nabrah.ai/ext/v1/webhooks/$ID -H "Authorization: Bearer $NABRAH_API_KEY" -H "Content-Type: application/json" -d '{"status":"active"}'Re-enabling clears the failure count too — without that, one more failure would disable it again immediately. Check GET /webhooks/{id}/deliveries first to see what was actually going wrong.
Redirects are never followed
A 3xx is a delivery failure, not a hop. Following one would hand the destination to whoever controls your endpoint's response, which would defeat the address checks we make before every send. Give us the final URL.
What we will and will not send to
The URL must be https, on port 443, with no credentials in it, and it must resolve to a public address. Private ranges, loopback, link-local and the cloud metadata addresses are all refused — including when they are reached indirectly, such as an IPv4-mapped or NAT64-wrapped address.
This is checked when you register the endpoint and again on every delivery, because a hostname that resolves publicly today can resolve privately tomorrow.
Verifying a delivery
Each request is signed with your endpoint's secret over timestamp.body. Compare in constant time, and reject anything older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(request: Request, body: string, secret: string) {
const timestamp = request.headers.get("X-Nabrah-Timestamp") ?? "";
const signature = request.headers.get("X-Nabrah-Signature") ?? "";
const age = Date.now() / 1000 - Number(timestamp);
if (!Number.isFinite(age) || Math.abs(age) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Sign against the raw body
Verify before parsing. Re-serialising the JSON changes key order and whitespace, and the signature will never match.