# Agent skills

> Everything an agent needs to use this API, in one file. Also served raw at /.well-known/skills.md.

**Tip: 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/v1`
- Machine-readable description: `https://notetaker.nabrah.ai/docs/api/openapi.json`
- MCP 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.txt`
- Any docs page as markdown: append `.md` to 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) and `offset`; each page carries `total`.
  `GET /usage/activity` is the exception: it takes `cursor` and returns `nextCursor`.
- 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-Remaining` and `X-RateLimit-Reset`; a spent
  window answers `429` with `Retry-After`.
- Writes accept `Idempotency-Key`. A retry with the same key returns the original
  response with `Idempotency-Replayed: true`. Always send one on `POST /bots`
  and `POST /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. For `meetings` and `aiCredits`:
  `planUsed` against `allowance` (what the plan paid for), `used` (including
  bonus), `available` (what this key's person can still start), `bonus[]`,
  `mine` and `myLimit` in a team, and `reached`. Also `minutesPerMeeting`,
  `maxMinutes`, `resetsAt` (the 1st) and `planRenewsAt` (the billing date).
  `null` always 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 /bots` and `POST /uploads` return a `usage` block —
  `{ 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 with `MEETING_TOO_LONG` and 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)
1. `POST /uploads { filename, title?, startsAt?, byteSize? }` — audio or transcript
   is decided from the file name. Counts as a meeting now.
2. `PUT /uploads/{uploadId}/content` with the raw file as the body (not multipart).
   Audio up to 500 MB; transcripts up to 20 MB.
3. Poll `GET /uploads/{uploadId}` or wait for `meeting.ready`.
- `POST /uploads/{uploadId}/retry` re-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 /bots` answers `409`; 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.failed` with
  `reason: "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.
