# Files and recordings

> Upload documents to a note, follow their text extraction, and read lecture recordings.

Source: https://workroad.app/docs/api/files

## Uploading files

A file upload takes three calls. The file itself goes straight to storage and never passes through the API.

1. `POST /notes/{noteId}/files` with `fileName`, `mimeType` and `sizeBytes`. The response has the file's `id`, a signed `upload.url` and the `upload.fields` to send with it.
2. POST the file to `upload.url` as `multipart/form-data`. Send every field from `upload.fields` in the order given, then the file itself as `file`, last.
3. `POST /files/{fileId}/complete`. It checks that storage has the file, starts extracting its text and returns 202 with the `jobId`. Poll `GET /files/{fileId}` until `status` is `ready` or `failed`.

```bash
curl "$UPLOAD_URL" \
   -F "bucket=workroad" \
   -F "key=note/<noteId>/<fileId>-lecture.pdf" \
   -F "Content-Type=application/pdf" \
   -F "x-amz-date=<value>" \
   -F "x-amz-algorithm=<value>" \
   -F "x-amz-credential=<value>" \
   -F "policy=<value>" \
   -F "x-amz-signature=<value>" \
   -F "file=@lecture.pdf"
```

Storage accepts the upload for 10 minutes after step 1 and refuses a file larger than `sizeBytes`, so send the exact size. Complete right after the upload, inside the same 10 minutes. The server keeps an uncompleted file for 24 hours after step 1, then deletes it and its row. Completing before the upload lands returns 409 `conflict` with `details.reason` `upload-missing`. Upload, then complete again. Completing checks the file's first bytes against its type, so a ZIP named `notes.pdf` returns 409 `conflict` with `details.reason` `wrong-type`, and the file is deleted.

The API takes documents up to 50 MiB: PDF, DOCX, PPTX, XLSX, ODT, ODS, ODP, RTF, EPUB, Markdown, HTML, CSV, LaTeX, Jupyter notebooks, plain text and source code. The file name's extension must match `mimeType`; the `mimeType` field of the create call lists each type. HTML, LaTeX and source code are sent as `text/plain`. Images are uploaded in the app.

Uploads are limited to 200 files and 2 GiB per user, and 25 files per note. Files on notes in the trash still count until permanent deletion. Over a limit, step 1 returns 409 `conflict` with `details.reason` set to `files-per-user`, `files-per-note` or `bytes-per-user`, plus `details.limit` and `details.used` in files or bytes. Restoring a note and deleting a file always remain available above these limits. Deleting a PDF preserves its embedded images in the note, and those images count toward the next upload's quota check. Delete files with `DELETE /files/{fileId}` or permanently delete trashed notes to make room.

A `failed` or `expired` file names the cause in `failure`. `upload-expired` and `object-expired` mean the 24 hours ran out and the upload is gone; start a new one. `object-missing` means storage had nothing when parsing started, `unsupported-format` and `too-large` mean the file can't be read, and `not-parsed` means nobody completed the upload. Completing a failed `document` again retries the parse. Completing a `ready` file returns 409 `conflict` with `details.reason` `already-parsed` and queues nothing. If the user already has too many jobs queued, completing returns 429 with `Retry-After: 5`.

`GET /notes/{noteId}/files` also lists images added in the app, with `kind` `image`. Only the app parses images, so completing one returns 409 `conflict` with `details.reason` `image-file`.

Anyone with a file's `url` can read the file once it is uploaded, without a key. Treat the URL as a secret.

## Recordings

`GET /notes/{noteId}/recordings` lists a note's lecture recordings, and `GET /recordings/{recordingId}` returns one with its transcript, a line per entry, once it is `finished`. `PUT /recordings/{recordingId}/summary` saves a `summary` and `keyTerms` you wrote from the transcript in place of the ones the recording had, and keeps its chapters. It returns 409 with `details.reason` `not-finished` while the recording is still coming in, or `summary-running` while WorkRoad's AI is writing a summary.

## List a note's files

`GET https://workroad.app/api/v1/notes/{noteId}/files`

Every file attached to the note, newest first, with its upload and parse status.

- MCP tool: `listNoteFiles` on `https://workroad.app/api/mcp`
- Required scopes: files:view
- Test mode: `GET https://workroad.app/api/test/v1/notes/{noteId}/files` checks the request and returns the example response without changing anything.

### Path parameters

- `noteId`: string (uuid), required. Note id, scoped to the key's user.

### Response

`200` OK

- `data`: array of objects, required
  - `id`: string (uuid), required
  - `noteId`: string (uuid), required
  - `kind`: one of `"document"`, `"image"`, required. `document` for files the API can upload and complete. `image` for images added in the app, which only the app parses.
  - `name`: string, required
  - `mimeType`: string, required
  - `sizeBytes`: integer or null, required. The size declared when the upload was created. Null for files uploaded before sizes were recorded.
  - `url`: string, required. Where the file is readable once uploaded. Anyone with this URL can read it.
  - `status`: one of `"awaiting_upload"`, `"parsing"`, `"ready"`, `"failed"`, `"expired"`, required. `awaiting_upload` until the file is uploaded and completed, `parsing` while its text is extracted, then `ready`, `failed` or `expired`.
  - `failure`: string or null, required. Why the file is `failed` or `expired`, such as `object-missing`, `unsupported-format` or `too-large`. Null otherwise.
  - `chars`: integer, required. Characters of extracted text.
  - `imageCount`: integer, required. Images extracted from the document.
  - `jobId`: string or null, required. The newest parse job for this file, when there is one.
  - `createdAt`: string (date-time), required. When the upload was created.

#### Example

```json
{
  "data": [
    {
      "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
      "noteId": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
      "kind": "document",
      "name": "Cell biology lecture.pdf",
      "mimeType": "application/pdf",
      "sizeBytes": 482113,
      "url": "https://storage.example.com/workroad/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf",
      "status": "ready",
      "failure": null,
      "chars": 18240,
      "imageCount": 3,
      "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
      "createdAt": "2026-01-15T09:30:00.000Z"
    }
  ]
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Start a file upload

`POST https://workroad.app/api/v1/notes/{noteId}/files`

Reserves a file on the note and returns a signed upload. POST the file to `upload.url` with the `upload.fields` within 10 minutes, then call `POST /files/{fileId}/complete`. A file that is never completed is deleted after 24 hours. Each user can hold 200 files and 2 GiB, and each note 25 files. Trashed files, and files on trashed notes, count until they are permanently deleted.

- MCP tool: `createNoteFile` on `https://workroad.app/api/mcp`
- Required scopes: files:upload
- Test mode: `POST https://workroad.app/api/test/v1/notes/{noteId}/files` checks the request and returns the example response without changing anything.

### Path parameters

- `noteId`: string (uuid), required. Note id, scoped to the key's user.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Request body

JSON.

- `fileName`: string, required, 1-255 characters. The file's name, shown in the app. Its extension must match `mimeType`, for example `notes.pdf` with `application/pdf`.
- `mimeType`: string, required, at least 1 character. The type for the file's extension: `application/pdf` (`.pdf`), `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (`.docx`), `application/vnd.openxmlformats-officedocument.presentationml.presentation` (`.pptx`), `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (`.xlsx`), `application/vnd.oasis.opendocument.text` (`.odt`), `application/vnd.oasis.opendocument.spreadsheet` (`.ods`), `application/vnd.oasis.opendocument.presentation` (`.odp`), `application/rtf` (`.rtf`), `application/epub+zip` (`.epub`), `text/markdown` (`.md`, `.markdown`), `text/csv` (`.csv`), `text/plain` (`.html`, `.htm`, `.txt`, `.text`, `.log`, `.tex`, `.py`, `.js`, `.mjs`, `.cjs`, `.jsx`, `.ts`, `.mts`, `.cts`, `.tsx`, `.go`, `.rs`, `.java`, `.c`, `.h`, `.cpp`, `.cc`, `.cxx`, `.hpp`, `.sh`, `.bash`, `.zsh`, `.sql`, `.yaml`, `.yml`, `.json`, `.css`, `.scss`, `.kt`, `.swift`, `.rb`, `.php`, `.cs`, `.r`, `.lua`, `.toml`, `.ini`, `.xml`), `application/x-ipynb+json` (`.ipynb`). HTML, LaTeX and source code are uploaded as `text/plain`. Images are uploaded in the app.
- `sizeBytes`: integer, required, 1 to 52428800. The exact size of the file in bytes, at most 52428800 (50 MiB). Storage refuses a larger upload.

### Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `status`: `"awaiting_upload"`, required
  - `upload`: object, required. A signed storage upload for at most `sizeBytes` bytes of `mimeType`.
    - `url`: string, required. POST the file here as multipart/form-data.
    - `fields`: array of objects, required. Form fields to send verbatim, in this order, before the `file` field.
      - `name`: string, required
      - `value`: string, required
  - `expiresAt`: string (date-time), required. Storage refuses the signed upload after this time, 10 minutes after creation.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
    "status": "awaiting_upload",
    "upload": {
      "url": "https://storage.example.com/workroad",
      "fields": [
        {
          "name": "bucket",
          "value": "workroad"
        },
        {
          "name": "key",
          "value": "note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf"
        },
        {
          "name": "Content-Type",
          "value": "application/pdf"
        },
        {
          "name": "x-amz-date",
          "value": "20260115T093000Z"
        },
        {
          "name": "x-amz-algorithm",
          "value": "AWS4-HMAC-SHA256"
        },
        {
          "name": "x-amz-credential",
          "value": "EXAMPLEKEY/20260115/us-east-1/s3/aws4_request"
        },
        {
          "name": "policy",
          "value": "eyJleHBpcmF0aW9uIjoiMjAyNi0wMS0xNVQwOTo0MDowMFoifQ=="
        },
        {
          "name": "x-amz-signature",
          "value": "9b2f0c1d8e7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c"
        }
      ]
    },
    "expiresAt": "2026-01-15T09:40:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: The file name's extension doesn't match a supported `mimeType`, or the body fails validation.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: A file quota is reached: `details.reason` is `files-per-user`, `files-per-note` or `bytes-per-user`, `details.limit` the limit and `details.used` what the user holds, in files or bytes. The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: The key is over a rate limit, or too many uploads by this user collided at once. Wait for the seconds in `Retry-After`. Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Complete a file upload

`POST https://workroad.app/api/v1/files/{fileId}/complete`

Checks that the upload reached storage and starts extracting the file's text. Poll `GET /files/{fileId}` until `status` is `ready` or `failed`. Calling it again while the file is parsing returns the same job.

- MCP tool: `completeNoteFile` on `https://workroad.app/api/mcp`
- Required scopes: files:upload
- Test mode: `POST https://workroad.app/api/test/v1/files/{fileId}/complete` checks the request and returns the example response without changing anything.

### Path parameters

- `fileId`: string (uuid), required. Note file id, scoped to the key's user.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Response

`202` Accepted

- `data`: object, required
  - `id`: string (uuid), required
  - `status`: `"parsing"`, required
  - `jobId`: string (uuid), required. The `file.parse` job extracting the text.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
    "status": "parsing",
    "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note file with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: `conflict` with `details.reason` `upload-missing` when storage has no upload for this file yet: upload it, then complete again. `conflict` with `details.reason` `image-file` for an image, which only the app parses. `conflict` with `details.reason` `already-parsed` when the file's text was already extracted (its `status` is `ready`). `conflict` with `details.reason` `wrong-type` when the uploaded bytes are not the type the file's extension names, for example a ZIP named `notes.pdf`; the file is deleted, so create it again with the right name. `idempotency_conflict` when the parse job's key collides with an earlier job; retry. The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: The user already has too many jobs queued. Wait for the seconds in `Retry-After` (5), then complete again. Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error
- `503`: The job queue is unavailable. Wait for the seconds in `Retry-After`, then complete again.

## Get a file

`GET https://workroad.app/api/v1/files/{fileId}`

The file's upload and parse status. `failure` explains a `failed` or `expired` file.

- MCP tool: `getNoteFile` on `https://workroad.app/api/mcp`
- Required scopes: files:view
- Test mode: `GET https://workroad.app/api/test/v1/files/{fileId}` checks the request and returns the example response without changing anything.

### Path parameters

- `fileId`: string (uuid), required. Note file id, scoped to the key's user.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `noteId`: string (uuid), required
  - `kind`: one of `"document"`, `"image"`, required. `document` for files the API can upload and complete. `image` for images added in the app, which only the app parses.
  - `name`: string, required
  - `mimeType`: string, required
  - `sizeBytes`: integer or null, required. The size declared when the upload was created. Null for files uploaded before sizes were recorded.
  - `url`: string, required. Where the file is readable once uploaded. Anyone with this URL can read it.
  - `status`: one of `"awaiting_upload"`, `"parsing"`, `"ready"`, `"failed"`, `"expired"`, required. `awaiting_upload` until the file is uploaded and completed, `parsing` while its text is extracted, then `ready`, `failed` or `expired`.
  - `failure`: string or null, required. Why the file is `failed` or `expired`, such as `object-missing`, `unsupported-format` or `too-large`. Null otherwise.
  - `chars`: integer, required. Characters of extracted text.
  - `imageCount`: integer, required. Images extracted from the document.
  - `jobId`: string or null, required. The newest parse job for this file, when there is one.
  - `createdAt`: string (date-time), required. When the upload was created.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
    "noteId": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "kind": "document",
    "name": "Cell biology lecture.pdf",
    "mimeType": "application/pdf",
    "sizeBytes": 482113,
    "url": "https://storage.example.com/workroad/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf",
    "status": "ready",
    "failure": null,
    "chars": 18240,
    "imageCount": 3,
    "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
    "createdAt": "2026-01-15T09:30:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note file with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Move a file to the trash

`DELETE https://workroad.app/api/v1/files/{fileId}`

The file disappears from every read and stops being searchable; `POST /files/{fileId}/restore` brings it back. Extracted images the note still shows are kept as standalone images, so the note doesn't change. If preserving those images would exceed a file or storage quota, nothing is moved. An image file is also removed from the note's content, and a restore doesn't put it back there. A trashed file keeps counting toward the storage limit until the trash is emptied after 30 days.

- MCP tool: `deleteNoteFile` on `https://workroad.app/api/mcp`
- Required scopes: files:delete
- Test mode: `DELETE https://workroad.app/api/test/v1/files/{fileId}` checks the request and returns the example response without changing anything.

### Path parameters

- `fileId`: string (uuid), required. Note file id, scoped to the key's user.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `deleted`: `true`, required

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
    "deleted": true
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: An image extracted from the file that the note still shows couldn't be copied out, so nothing was deleted.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note file with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The note changed while the file's images were being preserved, or preserving them would exceed a file or storage quota. Nothing was deleted. For a quota error, details has reason, limit and used.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Restore a file from the trash

`POST https://workroad.app/api/v1/files/{fileId}/restore`

Brings a trashed file back to its note's files and returns it. Needs `files:delete`, the scope that trashed it. The note must be out of the trash. An image file removed from the note's content stays out of it. The file never stopped counting toward the file and storage limits, so a restore always succeeds.

- MCP tool: `restoreNoteFile` on `https://workroad.app/api/mcp`
- Required scopes: files:delete
- Test mode: `POST https://workroad.app/api/test/v1/files/{fileId}/restore` checks the request and returns the example response without changing anything.

### Path parameters

- `fileId`: string (uuid), required. Note file id, scoped to the key's user.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `noteId`: string (uuid), required
  - `kind`: one of `"document"`, `"image"`, required. `document` for files the API can upload and complete. `image` for images added in the app, which only the app parses.
  - `name`: string, required
  - `mimeType`: string, required
  - `sizeBytes`: integer or null, required. The size declared when the upload was created. Null for files uploaded before sizes were recorded.
  - `url`: string, required. Where the file is readable once uploaded. Anyone with this URL can read it.
  - `status`: one of `"awaiting_upload"`, `"parsing"`, `"ready"`, `"failed"`, `"expired"`, required. `awaiting_upload` until the file is uploaded and completed, `parsing` while its text is extracted, then `ready`, `failed` or `expired`.
  - `failure`: string or null, required. Why the file is `failed` or `expired`, such as `object-missing`, `unsupported-format` or `too-large`. Null otherwise.
  - `chars`: integer, required. Characters of extracted text.
  - `imageCount`: integer, required. Images extracted from the document.
  - `jobId`: string or null, required. The newest parse job for this file, when there is one.
  - `createdAt`: string (date-time), required. When the upload was created.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
    "noteId": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "kind": "document",
    "name": "Cell biology lecture.pdf",
    "mimeType": "application/pdf",
    "sizeBytes": 482113,
    "url": "https://storage.example.com/workroad/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f-cell-biology-lecture.pdf",
    "status": "ready",
    "failure": null,
    "chars": 18240,
    "imageCount": 3,
    "jobId": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
    "createdAt": "2026-01-15T09:30:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note file with that id is in the trash for this key's user, on a note outside the trash.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## List a note's recordings

`GET https://workroad.app/api/v1/notes/{noteId}/recordings`

The lecture recordings on a note, newest first, without their transcripts. Read one with `GET /recordings/{recordingId}`.

- MCP tool: `listNoteRecordings` on `https://workroad.app/api/mcp`
- Required scopes: notes:view
- Test mode: `GET https://workroad.app/api/test/v1/notes/{noteId}/recordings` checks the request and returns the example response without changing anything.

### Path parameters

- `noteId`: string (uuid), required. Note id, scoped to the key's user.

### Response

`200` OK

- `data`: array of objects, required
  - `id`: string (uuid), required
  - `noteId`: string (uuid), required
  - `title`: string or null, required
  - `status`: one of `"recording"`, `"assembling"`, `"finished"`, required. `recording` while audio is still coming in, `assembling` while WorkRoad puts the transcript together, `finished` once it is final.
  - `summaryStatus`: one of `"none"`, `"queued"`, `"running"`, `"done"`, `"failed"`, required. Whether the recording has a summary: `done` when one is saved, `queued` or `running` while WorkRoad's AI writes one.
  - `createdAt`: string (date-time), required. When the recording started.

#### Example

```json
{
  "data": [
    {
      "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
      "noteId": "2f1e0d9c-8b7a-4c6d-9e5f-4a3b2c1d0e9f",
      "title": "Lecture 7: CPU scheduling",
      "status": "finished",
      "summaryStatus": "done",
      "createdAt": "2026-01-15T09:30:00.000Z"
    }
  ]
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No note with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get a recording with its transcript

`GET https://workroad.app/api/v1/recordings/{recordingId}`

The recording's transcript, one line per entry, and its summary, key terms and chapters when it has them. Write notes, flashcards or a practice test from the transcript with your own model, then save them with `POST /notes`, `POST /sets` or `POST /tests/from-questions`.

- MCP tool: `getRecording` on `https://workroad.app/api/mcp`
- Required scopes: notes:view
- Test mode: `GET https://workroad.app/api/test/v1/recordings/{recordingId}` checks the request and returns the example response without changing anything.

### Path parameters

- `recordingId`: string (uuid), required. Recording id from `GET /notes/{noteId}/recordings`.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `noteId`: string (uuid), required
  - `title`: string or null, required
  - `status`: one of `"recording"`, `"assembling"`, `"finished"`, required. `recording` while audio is still coming in, `assembling` while WorkRoad puts the transcript together, `finished` once it is final.
  - `summaryStatus`: one of `"none"`, `"queued"`, `"running"`, `"done"`, `"failed"`, required. Whether the recording has a summary: `done` when one is saved, `queued` or `running` while WorkRoad's AI writes one.
  - `createdAt`: string (date-time), required. When the recording started.
  - `transcript`: array of string, required. The transcript, one entry per line, in order. Empty until the recording is finished.
  - `summary`: string or null, required. The saved summary, or `null`.
  - `keyTerms`: array of objects, required
    - `term`: string, required
    - `definition`: string, required
  - `chapters`: array of objects, required
    - `startMs`: integer, required
    - `title`: string, required
    - `summary`: string, required

#### Example

```json
{
  "data": {
    "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
    "noteId": "2f1e0d9c-8b7a-4c6d-9e5f-4a3b2c1d0e9f",
    "title": "Lecture 7: CPU scheduling",
    "status": "finished",
    "summaryStatus": "done",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "transcript": [
      "Today we look at how the scheduler picks the next process.",
      "Round robin gives each process a fixed time slice, in turn."
    ],
    "summary": "How a CPU scheduler chooses the next process, with round robin as the main example.",
    "keyTerms": [
      {
        "term": "Round robin",
        "definition": "Each process runs for a fixed time slice, in turn."
      }
    ],
    "chapters": [
      {
        "startMs": 0,
        "title": "Scheduling",
        "summary": "Why the CPU needs a scheduler."
      }
    ]
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No recording with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Save a recording's summary

`PUT https://workroad.app/api/v1/recordings/{recordingId}/summary`

Saves a summary and key terms you wrote from the transcript, replacing the ones the recording had. Chapters stay as they are. The recording page shows them as it shows the ones WorkRoad's AI writes.

Use this to save content you wrote yourself. It uses none of the user's WorkRoad AI uses.

- MCP tool: `putRecordingSummary` on `https://workroad.app/api/mcp`
- Required scopes: notes:update
- Test mode: `PUT https://workroad.app/api/test/v1/recordings/{recordingId}/summary` checks the request and returns the example response without changing anything.

### Path parameters

- `recordingId`: string (uuid), required. Recording id from `GET /notes/{noteId}/recordings`.

### Request body

JSON.

- `summary`: string, required, 1-4000 characters. What the lecture covered, in plain text, up to 4000 characters.
- `keyTerms`: array of objects, up to 40 items, default `[]`. Up to 40 terms the lecture defines, each once. The app can make a study set from them.
  - `term`: string, required, 1-80 characters
  - `definition`: string, required, 1-400 characters

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `noteId`: string (uuid), required
  - `title`: string or null, required
  - `status`: one of `"recording"`, `"assembling"`, `"finished"`, required. `recording` while audio is still coming in, `assembling` while WorkRoad puts the transcript together, `finished` once it is final.
  - `summaryStatus`: one of `"none"`, `"queued"`, `"running"`, `"done"`, `"failed"`, required. Whether the recording has a summary: `done` when one is saved, `queued` or `running` while WorkRoad's AI writes one.
  - `createdAt`: string (date-time), required. When the recording started.
  - `transcript`: array of string, required. The transcript, one entry per line, in order. Empty until the recording is finished.
  - `summary`: string or null, required. The saved summary, or `null`.
  - `keyTerms`: array of objects, required
    - `term`: string, required
    - `definition`: string, required
  - `chapters`: array of objects, required
    - `startMs`: integer, required
    - `title`: string, required
    - `summary`: string, required

#### Example

```json
{
  "data": {
    "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
    "noteId": "2f1e0d9c-8b7a-4c6d-9e5f-4a3b2c1d0e9f",
    "title": "Lecture 7: CPU scheduling",
    "status": "finished",
    "summaryStatus": "done",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "transcript": [
      "Today we look at how the scheduler picks the next process.",
      "Round robin gives each process a fixed time slice, in turn."
    ],
    "summary": "How a CPU scheduler chooses the next process, with round robin as the main example.",
    "keyTerms": [
      {
        "term": "Round robin",
        "definition": "Each process runs for a fixed time slice, in turn."
      }
    ],
    "chapters": [
      {
        "startMs": 0,
        "title": "Scheduling",
        "summary": "Why the CPU needs a scheduler."
      }
    ]
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No recording with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: `conflict` with `details.reason` `not-finished` while the recording is still coming in or assembling, or `summary-running` while WorkRoad's AI is writing its summary. Try again once it is done.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error
