# Openwhispr API

> Use this skill when building integrations with the OpenWhispr REST API, calling OpenWhispr endpoints, managing notes/folders/transcriptions programmatically, accessing team-space content with workspace keys, or connecting to the OpenWhispr MCP server. Covers authentication, all V1 endpoints, spaces, pagination, rate limits, error handling, and the remote MCP server.

- Skill: `openwhispr/openwhispr-api` (Agent Skill)
- Install (CLI): `npx skillmds@latest add openwhispr/openwhispr-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openwhispr/openwhispr-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: openwhispr (https://skillmd.com/u/openwhispr)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/openwhispr/openwhispr-api

---


# OpenWhispr API v1

Use this reference when making requests to the OpenWhispr REST API. All endpoints are under the V1 path and require API key authentication.

## Authentication

Pass the API key as a Bearer token in the `Authorization` header on every request.

```
Authorization: Bearer owk_live_YOUR_KEY
```

There are two kinds of key:

- **Personal keys** (`owk_live_`) — access your own private notes, folders, and transcriptions. Generated under **Settings > API Keys**.
- **Workspace keys** (`ow_wks_live_`) — access a workspace's **team spaces**. Generated by a workspace admin under **Settings > Workspace > Developer**.

Both are shown once at creation.

### Scopes

Each key has scoped permissions. The API rejects requests missing the required scope with `403 Forbidden`.

**Personal key scopes:**

| Scope                 | Grants                                            |
| --------------------- | ------------------------------------------------- |
| `notes:read`          | List, get, and search notes. List folders.        |
| `notes:write`         | Create, update, and delete notes. Create folders. |
| `transcriptions:read` | List and get transcriptions.                      |
| `usage:read`          | Read usage statistics.                            |

**Workspace key scopes:**

| Scope                           | Grants                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------- |
| `workspace:notes:read`          | List, get, and search team-space notes.                                         |
| `workspace:notes:write`         | Create, update, and delete team-space notes.                                    |
| `workspace:folders:read`        | List team-space folders.                                                        |
| `workspace:folders:write`       | Create team-space folders.                                                      |
| `workspace:transcriptions:read` | Space discovery only for now — no transcription endpoints accept workspace keys yet. |
| `workspace:*`                   | All of the above (admin).                                                       |

Any of the content scopes above also grants `GET /spaces/list` (space discovery).

### Team spaces

A **space** is a shared container of notes and folders inside a workspace. Personal keys never see team-space content; **workspace keys** do, and always address one space at a time via a `space_id`:

- Discover the spaces a key can reach with `GET /spaces/list`.
- `list`, `create`, and `search` for notes and folders **require** a `space_id` (query param or body field) when called with a workspace key, and reject one when called with a personal key.
- Operations addressed by note id (`GET/PATCH/DELETE /notes/{id}`, `GET /notes/{id}/transcript`) resolve the note's space automatically — no `space_id` needed. A workspace key may act on any note in any of its workspace's spaces.
- `space_id` cannot be changed through the API — a note stays in the space it was created in. Move notes between spaces from the desktop app.

## Base URL

```
https://api.openwhispr.com/api/v1
```

## Response Envelope

Wrap all responses in a consistent envelope.

**Single resource:**

```json
{ "data": { "id": "uuid", "title": "My note", ... } }
```

**Paginated list:**

```json
{
  "data": [{ ... }, { ... }],
  "has_more": true,
  "next_cursor": "opaque-cursor-string"
}
```

**Error:**

```json
{ "error": { "code": "not_found", "message": "Note not found" } }
```

### Error Codes

| HTTP Status | Code                 | Meaning                                            |
| ----------- | -------------------- | -------------------------------------------------- |
| 400         | `validation_error`   | Invalid request body or query params               |
| 401         | `invalid_api_key`    | Missing, malformed, expired, or revoked key        |
| 403         | `forbidden`          | Key lacks required scope                           |
| 404         | `not_found`          | Resource does not exist or belongs to another user |
| 405         | `method_not_allowed` | Wrong HTTP method                                  |
| 409         | `conflict`           | Duplicate resource (e.g. folder name)              |
| 429         | `rate_limited`       | Rate limit exceeded — check `Retry-After` header   |
| 500         | `internal_error`     | Server error                                       |

## Rate Limits

Enforced per API key with minute and daily windows. Search requests cost 5x against the rate limit.

| Plan     | Per Minute | Per Day |
| -------- | ---------- | ------- |
| Free     | 30         | 1,000   |
| Pro      | 120        | 10,000  |
| Business | 300        | 50,000  |

Response headers on every request:

| Header                  | Description                       |
| ----------------------- | --------------------------------- |
| `X-RateLimit-Limit`     | Max requests per minute           |
| `X-RateLimit-Remaining` | Remaining in current window       |
| `X-RateLimit-Reset`     | Unix timestamp when window resets |
| `Retry-After`           | Seconds to wait (only on 429)     |

## Pagination

List endpoints use cursor-based pagination. Treat `next_cursor` as an **opaque string**: pass it back verbatim as the `cursor` query parameter to fetch the next page — never parse it. (Notes cursors are base64url-encoded composites; transcription cursors are timestamps. Timestamp cursors issued before the composite format remain accepted.) When `has_more` is `false`, there are no more results. An unparseable cursor returns `400 validation_error`.

```
GET /notes/list?limit=50&cursor=NEXT_CURSOR_FROM_PREVIOUS_RESPONSE
```

## Endpoints

### Spaces (workspace keys only)

**List Spaces** — `GET /spaces/list`
Scope: any workspace content scope (`workspace:notes:read/write`, `workspace:folders:read/write`, or `workspace:transcriptions:read`). Returns the non-archived team spaces in the key's workspace (`id`, `name`, `slug`, `description`, `emoji`, `created_at`, `updated_at`). Use a returned `id` as the `space_id` on notes/folders requests.

### Notes

**List Notes** — `GET /notes/list`
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `limit` | integer | No | 1-100, default 50 |
| `cursor` | string | No | Pagination cursor |
| `folder_id` | UUID | No | Filter by folder |
| `space_id` | UUID | Workspace keys | Team space to list. Required for workspace keys; rejected for personal keys. |
Scope: `notes:read` (personal) / `workspace:notes:read` (workspace)

**Get Note** — `GET /notes/{id}`
Scope: `notes:read` / `workspace:notes:read`. Returns 404 if the note does not exist or is deleted. A workspace key may fetch any note in its workspace's spaces.

**Create Note** — `POST /notes/create`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | Yes | Note body text |
| `title` | string | No | Note title |
| `enhanced_content` | string | No | Cleaned/enhanced version |
| `note_type` | enum | No | `personal` (default), `meeting`, `upload` |
| `folder_id` | UUID | No | Target folder |
| `space_id` | UUID | Workspace keys | Team space to create in. Required for workspace keys; rejected for personal keys. |
Scope: `notes:write` / `workspace:notes:write`. Returns `201` with the created note.

**Update Note** — `PATCH /notes/{id}`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | No | New title |
| `content` | string | No | New content |
| `enhanced_content` | string | No | New enhanced content |
| `folder_id` | UUID | No | Move to folder |
Scope: `notes:write` / `workspace:notes:write`. All fields optional — only provided fields are updated. Cannot change a note's space.

**Delete Note** — `DELETE /notes/{id}`
Scope: `notes:write` / `workspace:notes:write`. Soft-deletes the note. Returns `204 No Content`.

**Search Notes** — `POST /notes/search`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | Yes | Search text (1-500 chars) |
| `limit` | integer | No | 1-50, default 20 |
| `space_id` | UUID | Workspace keys | Team space to search. Required for workspace keys; rejected for personal keys. |
Scope: `notes:read` / `workspace:notes:read`. Uses hybrid semantic (vector) + full-text search with relevance scoring. Costs 5x against rate limit.

### Folders

**List Folders** — `GET /folders/list`
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `space_id` | UUID | Workspace keys | Team space to list. Required for workspace keys; rejected for personal keys. |
Scope: `notes:read` / `workspace:folders:read`. Returns all folders sorted by `sort_order` then `created_at`.

**Create Folder** — `POST /folders/create`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Folder name (1-100 chars) |
| `sort_order` | integer | No | Sort position |
| `space_id` | UUID | Workspace keys | Team space to create in. Required for workspace keys; rejected for personal keys. |
Scope: `notes:write` / `workspace:folders:write`. Max 50 folders per user. Returns `409` if name already exists.

### Transcriptions

**List Transcriptions** — `GET /transcriptions/list`
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `limit` | integer | No | 1-100, default 50 |
| `cursor` | string | No | Pagination cursor |
Scope: `transcriptions:read`. Returns transcription history with `text`, `word_count`, `source`, `provider`, `model`, `language`, `audio_duration_ms`, `processing_ms`.

**Get Transcription** — `GET /transcriptions/{id}`
Scope: `transcriptions:read`.

### Usage

**Get Usage** — `GET /usage`
Scope: `usage:read`. Returns:

- `words_used` — Words consumed this period
- `words_remaining` — Words left in quota
- `limit` — Total word quota
- `plan` — Current plan (`free`, `pro`, `business`)
- `is_subscribed` — Whether user has active subscription
- `current_period_end` — End of current billing period
- `billing_interval` — Billing cycle

## MCP Server

For AI assistant integration (Claude, Cursor, VS Code), connect to the remote MCP server at:

```
https://mcp.openwhispr.com/mcp
```

Pass the API key via `Authorization: Bearer` header. All V1 endpoints are available as MCP tools. The server uses Streamable HTTP transport (stateless, no sessions).

### Claude Code

```bash
claude mcp add openwhispr --transport http https://mcp.openwhispr.com/mcp \
  --header "Authorization: Bearer owk_live_YOUR_KEY"
```

### Cursor / VS Code

```json
{
  "mcpServers": {
    "openwhispr": {
      "url": "https://mcp.openwhispr.com/mcp",
      "headers": { "Authorization": "Bearer owk_live_YOUR_KEY" }
    }
  }
}
```

## Examples

### List recent notes

```bash
curl -H "Authorization: Bearer owk_live_YOUR_KEY" \
  "https://api.openwhispr.com/api/v1/notes/list?limit=10"
```

### Create a note in a folder

```bash
curl -X POST \
  -H "Authorization: Bearer owk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Remember to review PR #42", "title": "TODO", "folder_id": "UUID"}' \
  https://api.openwhispr.com/api/v1/notes/create
```

### Search notes

```bash
curl -X POST \
  -H "Authorization: Bearer owk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "quarterly budget discussion"}' \
  https://api.openwhispr.com/api/v1/notes/search
```

### Paginate through all notes

```bash
cursor=""
while true; do
  response=$(curl -s -H "Authorization: Bearer owk_live_YOUR_KEY" \
    "https://api.openwhispr.com/api/v1/notes/list?limit=100&cursor=${cursor}")
  echo "$response" | jq '.data[]'
  has_more=$(echo "$response" | jq -r '.has_more')
  [ "$has_more" != "true" ] && break
  cursor=$(echo "$response" | jq -r '.next_cursor')
done
```

### Check usage

```bash
curl -H "Authorization: Bearer owk_live_YOUR_KEY" \
  https://api.openwhispr.com/api/v1/usage
```

### Work with team spaces (workspace key)

```bash
# 1. Discover the spaces this workspace key can reach
curl -H "Authorization: Bearer ow_wks_live_YOUR_KEY" \
  https://api.openwhispr.com/api/v1/spaces/list

# 2. List notes in a space (space_id is required for workspace keys)
curl -H "Authorization: Bearer ow_wks_live_YOUR_KEY" \
  "https://api.openwhispr.com/api/v1/notes/list?space_id=SPACE_UUID&limit=50"

# 3. Create a note in that space
curl -X POST \
  -H "Authorization: Bearer ow_wks_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Kickoff notes", "title": "Kickoff", "space_id": "SPACE_UUID"}' \
  https://api.openwhispr.com/api/v1/notes/create
```

