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

EventFires when
meeting.startedThe notetaker has joined the call
meeting.endedThe call finished; processing has begun
meeting.readyTranscript and summary are available
meeting.failedThe notetaker could not join, or processing failed
calendar.connectedSomeone finished connecting a calendar
calendar.disconnectedA calendar was disconnected or its access was revoked

The payload

POSThttps://your-app.example.com/hooks/nabrah

Every delivery carries the event name, a timestamp, and the affected record.

Body
{
  "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"
  }
}
Response 200
{ "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:

Shell
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.

Delivery guarantees