Core concepts
Agent skills
Everything an agent needs to use this API, in one file. Also served raw at /.well-known/skills.md.
The same document, raw
Agents can load it from https://notetaker.nabrah.ai/.well-known/skills.md.
A meeting notetaker. It watches a calendar, sends a bot into Google Meet, Teams and Webex calls, and turns each recording into a summary, action items and a searchable transcript in Arabic or English. Recordings made elsewhere can be uploaded and get the same notes.
Base URL:
https://api-notetaker.nabrah.ai/ext/v1Machine-readable description:
https://notetaker.nabrah.ai/docs/api/openapi.jsonMCP server:
https://api-notetaker.nabrah.ai/mcp(same key)Human docs: https://notetaker.nabrah.ai/en/docs/api
Docs index:
https://notetaker.nabrah.ai/llms.txt, every page in one file:/llms-full.txtAny docs page as markdown: append
.mdto its URL
Authentication
Send Authorization: Bearer nt_… on every request. Keys are created by a person at https://notetaker.nabrah.ai/en/dashboard/developers and are either:
| Access | Allows |
|---|---|
readonly | GET only. Any write answers 403. |
full | Everything below, including sending and stopping bots. |
A key from an organization owner or admin is organization-wide: the /organization endpoints answer. Every other key is personal, and those endpoints answer 404. Call GET /me first to learn which you hold.
Envelope, errors, paging
Success:
{ "data": … }. Failure:{ "error": { "code", "message", "details"? } }.Lists:
limit(1–100, default 25) andoffset; each page carriestotal.GET /usage/activityis the exception: it takescursorand returnsnextCursor.Anything belonging to another account answers 404, never 403.
Rate limit: 600 requests per 15 minutes per key. Every response carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; a spent window answers429withRetry-After.Writes accept
Idempotency-Key. A retry with the same key returns the original response withIdempotency-Replayed: true. Always send one onPOST /botsandPOST /uploads.
| Status | code | What to do |
|---|---|---|
| 401 | UNAUTHORIZED | The key is missing, unknown, revoked or expired. Stop. |
| 403 | FORBIDDEN | A read-only key tried a write. Stop. |
| 404 | NOT_FOUND | Not there, or not this key's to see. |
| 409 | CONFLICT | The state does not allow it (e.g. a bot is already live). Do not retry. |
| 409 | RECORDING_QUOTA_REACHED | This month's meetings are used. Resets on the 1st. |
| 409 | MEETING_TOO_LONG | Longer than the plan's minutes per meeting. |
| 409 | UPLOAD_PENDING_LIMIT | Three uploads are still waiting for their file. |
| 409 | UPLOAD_EXPIRED | The file came too late; create a new upload. |
| 409 | USAGE_RETRY | The team changed mid-request. Send it again now. |
| 422 | VALIDATION_ERROR | Fix the body; details names the field. |
| 429 | RATE_LIMITED | Wait Retry-After seconds. |
| 5xx | INTERNAL_ERROR | Retry with backoff. |
Usage refusals (RECORDING_QUOTA_REACHED, MEETING_TOO_LONG, UPLOAD_PENDING_LIMIT, USAGE_RETRY) are never remembered by Idempotency-Key, so the same request succeeds once there is room.
Usage and limits
Plans limit three things per calendar month: meetings, minutes per meeting, and AI credits (used by chat in the app). In a team the monthly allowance is shared by everyone; a team may also set a limit on one person. People can hold bonus meetings or credits granted by support.
GET /usage— the month so far. FormeetingsandaiCredits:planUsedagainstallowance(what the plan paid for),used(including bonus),available(what this key's person can still start),bonus[],mineandmyLimitin a team, andreached. AlsominutesPerMeeting,maxMinutes,resetsAt(the 1st) andplanRenewsAt(the billing date).nullalways means unlimited, never zero.GET /usage/activity?kind=meeting|ai_credit&cursor&limit— every entry that used a meeting or credit, and every bonus grant, newest first.POST /botsandPOST /uploadsreturn ausageblock —{ remaining, maxMinutes, resetsAt }— so you can check before the next one.Deleting a recording never gives a meeting back. Only a bot that was never let in, a failed upload, or an upload that was refused is not counted.
A bot leaves when the meeting reaches
maxMinutes. An uploaded file longer than that is refused withMEETING_TOO_LONGand not counted.
What you can do
Know who you are
| Goal | Call |
|---|---|
| Which account, plan and key type this is | GET /me |
| What is left this month | GET /usage |
Calendar and schedule
| Goal | Call |
|---|---|
| Upcoming meetings, and whether the bot will join each (with the reason if not) | GET /meetings/schedule?from&to |
| Meetings on the calendar | GET /meetings?from&to |
| One meeting, with its bot status and notes | GET /meetings/{id} |
| Add a meeting by hand | POST /meetings |
| Force recording on or off for one meeting | PATCH /meetings/{id} { recordOverride } with true, false or null |
| Connected calendars and their auto-join rules | GET /calendar/connections |
| Change auto-join mode or rules | PATCH /calendar/connections/{id} |
| Connect a calendar | POST /calendar/connect-links { provider } with "google", "microsoft" (Outlook / Teams) or "webex" — returns a URL a person must open |
Bots
| Goal | Call |
|---|---|
| Send the notetaker into a call now | POST /bots { joinUrl, title? } |
| Check on it | GET /bots/{runId} |
| Pull it out | POST /bots/{runId}/stop |
| Recent bot runs | GET /bots |
Recordings and notes
| Goal | Call |
|---|---|
| Finished and in-progress recordings | GET /recordings |
| Summary, attendees and action items | GET /recordings/{runId} |
| Same, with the transcript | GET /recordings/{runId}?include=transcript |
| The transcript, paged | GET /recordings/{runId}/transcript?limit&offset |
| Action items | GET /recordings/{runId}/action-items |
| Tick or reopen one | PATCH /action-items/{itemId} { status } with "done" or "active" |
Download the audio (supports Range) | GET /recordings/{runId}/audio |
Uploads (recordings made elsewhere)
POST /uploads { filename, title?, startsAt?, byteSize? }— audio or transcript is decided from the file name. Counts as a meeting now.PUT /uploads/{uploadId}/contentwith the raw file as the body (not multipart). Audio up to 500 MB; transcripts up to 20 MB.Poll
GET /uploads/{uploadId}or wait formeeting.ready.
POST /uploads/{uploadId}/retryre-runs a failed analysis on the stored file.DELETE /uploads/{uploadId}removes it (the meeting stays counted if the file arrived).
Organization (organization-wide keys only)
| Goal | Call |
|---|---|
| Seats, this month's usage, what needs attention | GET /organization |
| Members and one member | GET /organization/members, GET /organization/members/{userId} |
| Usage over time | GET /organization/analytics?days (totals, weekly series, members, and insights: change vs the previous window, join rate, weekdays, sources, AI questions) |
| Invitations | GET /organization/invites, POST /organization/invites |
Webhooks
| Goal | Call |
|---|---|
| Register an endpoint | POST /webhooks { url, events } — the response holds the signing secret, once |
| List, read, change, delete | GET/PATCH/DELETE /webhooks/{id} |
| Send a test event | POST /webhooks/{id}/test |
| What was sent and how it went | GET /webhooks/{id}/deliveries |
Events: meeting.started, meeting.ended, meeting.ready, meeting.failed, meeting.skipped (not recorded: this month's meetings are used), usage.threshold_reached (80% or 100% of a monthly limit), calendar.connected, calendar.disconnected, calendar.reauth_required.
Verify every delivery before parsing: HMAC-SHA256 of ${X-Nabrah-Timestamp}.${raw body} with the endpoint secret, hex, compared in constant time with X-Nabrah-Signature; reject timestamps older than five minutes. Answer 2xx within ten seconds. Failures are retried six times over about a minute; after that, read the deliveries list. Events can arrive out of order — sort by createdAt.
Recipes
Record a call happening now. GET /usage (is meetings.available above 0?) → POST /bots with an Idempotency-Key → wait for meeting.ready, or poll GET /bots/{runId} → GET /recordings/{runId}.
Record everything on a calendar. POST /calendar/connect-links → hand the URL to the person → GET /calendar/connections until it appears → PATCH its join mode → check GET /meetings/schedule for what will be recorded.
Pull notes into another system. POST /webhooks { events: ["meeting.ready"] } → on each event, GET /recordings/{runId} (and /transcript if needed) → tick action items back with PATCH /action-items/{itemId}.
Warn before running out. Subscribe to usage.threshold_reached and meeting.skipped, or read GET /usage daily and compare planUsed with allowance.
Rules worth knowing before you try
One live bot per account. A second
POST /botsanswers409; retrying will not help. Wait for the first, or stop it.Only Google Meet, Microsoft Teams and Webex links are accepted. Zoom is not.
The calendar holds roughly 7 days back and 30 forward. Beyond that the honest answer is "not synced", not "nothing scheduled".
A meeting under three minutes is not transcribed (
meeting.failedwithreason: "too_short") and not counted.Ticking an action item marks it as a person's, so a later re-analysis will not reopen it.
Connecting a calendar always needs a person in a browser; never try to do it headlessly.
A key issued by an organization admin is revoked the moment they leave the organization.