# Notes

> Read and write notes as Markdown, avoid overwriting edits, and organise notes into folders.

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

## Writing notes

Note content goes in as Markdown and comes out two ways. `GET /notes/{noteId}` returns `content`, the editor's JSON document, and `markdown`, the same document converted back. The server stores only the editor JSON, so the Markdown you read back is a conversion, not your original text.

`POST /notes` takes optional `markdown` for the first version of a note. Two operations write an existing note. `PUT /notes/{noteId}/content` replaces the whole document. `POST /notes/{noteId}/content/append` adds your blocks after the current ones and keeps their ids. Send an `Idempotency-Key` with the append so a retry can't add the blocks twice; without a key, send `baseVersion` and a retry fails with 409 instead. `PUT` takes no `mode`, and a body with one returns 400. An empty or whitespace-only `markdown` on an append returns 400 `invalid_request` with `details.reason` `empty-append`. An append drops one empty paragraph at the end of the note first, so a new or app-saved note doesn't gain a blank block. A note holds at most 200,000 visible characters, so an append that would pass that returns 400 `invalid_request` with `details.reason` `note-too-large`. A replace is not capped by it. Raw HTML in the Markdown is shown as text, except `<u>` and, inside a table cell, `<br>`. `GET /notes/{noteId}` writes the note's inline images as `![alt](src)`. A replace keeps every image that stays in your Markdown and deletes the ones you leave out from storage, so reading a note and writing its `markdown` back keeps all of them. An append keeps them all. An image that points at an upload the web editor hasn't confirmed yet can still be deleted 24 hours after the upload.

Writing a note's `markdown` back with `PUT /notes/{noteId}/content` gives the same document when `markdownLosses` is empty. That includes an image's alt text and title, which stay text even when they look like a number, such as `![2024](x.png)`. An image with an empty alt or title reads back with `null`, and so does a link with an empty title. When it isn't, the listed parts come back simplified, as described in [Markdown in notes](#markdown-in-notes). Block ids exist only in `content`, and a write gives the blocks new ones. A note whose content isn't an editor document returns `markdown: null`, and an append to it returns 400 with `details.reason` `legacy-content`.

Every content change raises the note's `version`. To avoid overwriting a save made in the app or by another client, send the `version` you read as `baseVersion`. If the note changed since, the write returns 409 `conflict` with `details.baseVersion` and `details.currentVersion` and writes nothing. Read the note again, merge, and retry with the new version. Without `baseVersion` the write always goes through.

A `folderId` in a body that is not one of the user's folders returns 400 `invalid_request` with `details.field` `folderId` and `details.reason` `unknown-folder`. It works the same on `POST /notes` and `PATCH /notes/{noteId}`.

`PATCH /notes/{noteId}` renames a note, moves it into a folder or out of one (`folderId: null`), and pins it. `DELETE /notes/{noteId}` moves the note to the trash, where it drops out of every read, and `POST /notes/{noteId}/restore` brings it back within 30 days. Both need `notes:delete`.

## Markdown in notes

Notes use CommonMark with GFM tables and strikethrough, plus a few additions for what the editor has and plain Markdown doesn't. `GET /notes/{noteId}` writes them, and every write endpoint reads them.

| Content                    | Markdown                                                                                                                              |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Task list                  | `- [ ] open` and `- [x] done`. Every item in the list needs a checkbox, or the list stays a plain list.                               |
| Inline math                | `$x^2$`. No space after the opening `$` or before the closing one, and no digit right after it, so `$5 and $10` stays text.           |
| Block math                 | `$$` on its own line, the latex, then `$$` on its own line.                                                                           |
| Mention                    | `[@Cell biology](workroad://note/<noteId>)`. The kind is one of `note`, `set`, `subject`, `task`, `recording`, `test` and `schedule`. |
| Underline                  | `<u>underlined</u>`                                                                                                                   |
| Line break in a table cell | `<br>`                                                                                                                                |
| Footnote                   | `[^1]` in the text and `[^1]: text` on a line of its own. A footnote's further blocks follow, indented four spaces.                   |
| Toggle                     | `<details><summary>Summary</summary>`, a blank line, the body blocks, then `</details>` on its own line. The summary is plain text.   |
| Callout                    | A quote whose first line is only `[!NOTE]`, `[!TIP]`, `[!WARNING]` or `[!DANGER]`; the rest of the quote is the callout's body.       |
| Table of contents          | A `[TOC]` line. It is written but not read back: writing the Markdown again gives a paragraph with that text.                         |

Headings 5 and 6 become heading 4, the deepest the editor has. Two lists next to each other alternate between `-` and `*`, because CommonMark joins two lists with the same bullet into one.

`markdownLosses` lists what a note's `markdown` can't carry. The values are:

**Every markdownLosses value**

- `empty-paragraphs`: empty paragraphs are left out. One at the end of the note doesn't count, because the editor adds it back.
- `collapsed-spaces`: runs of spaces become one space, and spaces at the start or end of a paragraph or next to a line break are dropped. A line break at the end of a paragraph is dropped too, and one inside a heading becomes a space.
- `image-size`: image width and height are left out.
- `table-layout`: merged cells, column widths and cell alignment are left out.
- `table-cell-blocks`: a cell with a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph becomes one line, with `<br>` between its paragraphs, headings and toggle summaries. A formula becomes inline math and code becomes its text. List bullets, checkboxes, quote and callout markers, images and embeds in the cell are left out. Footnote markers, math, citations, dates and mentions keep their usual form.
- `table-header`: Markdown tables always have a header row, so the first row becomes one.
- `table-cell-background`: cell background colours are left out.
- `ordered-list-type`: letter and Roman numbering become numbers.
- `image-upload-state`: an image that never finished uploading is written as its alt text.
- `inline-math`: inline math that `$…$` can't hold is written as text, such as math followed directly by a digit. Spaces at either end of the latex are trimmed.
- `mention-label`: a mention without a label gets its kind as the label.
- `formatting-edges`: bold, italic and strikethrough can't start or end on a space, so the space moves outside. Where Markdown can't start or end them at all, such as bold text that starts with punctuation right after a letter, the paragraph loses its bold, italic and strikethrough.
- `highlight`: highlighting is dropped while its text is kept.
- `text-colour`: text colour is dropped while the text is kept.
- `toggle-summary-formatting`: a toggle's summary becomes plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting.
- `toggle-heading-level`: a toggle whose summary reads as a heading keeps the heading's text and loses its level, because a `<summary>` tag carries the text and nothing else.
- `code-block-language`: a code block language that can't fit in a Markdown fence is left out.
- `code-block-filename`: a code block's filename is left out, because a fence carries the language and nothing else.
- `code-block-wrap`: a code block's soft-wrap setting is left out. The code itself is unchanged.
- `block-math`: a formula line containing only `$$` becomes `$ $`.
- `image-address`: an image address is normalized, or the image becomes alt text if its address is refused.
- `image-loading`: the image loading preference becomes lazy.
- `link-address`: a link address is normalized, or the link becomes text if its address is refused.
- `link-title`: a numeric or boolean link title comes back as that value instead of text.
- `link-attributes`: the link target, rel or CSS class returns to the editor default.
- `mention-target`: a mention with an invalid kind or ID becomes plain text.
- `date-chip`: an inline date is written as its day in `YYYY-MM-DD` form, as plain text.
- `heading-level`: an unsupported heading level becomes level 1 or 4.
- `ordered-list-start`: a list start that Markdown can't represent becomes 1.
- `callout-kind`: a callout of an unknown kind is written as a note.
- `callout-nesting`: a callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind.
- `youtube-embed`: a YouTube block is written as a link to the video; reading the Markdown back gives the link, not the player.
- `pdf-embed`: a PDF block is written as a link to the file; reading the Markdown back gives the link, not the viewer.
- `recording-embed`: a recording's audio player is written as its label. The recording itself isn't part of the Markdown, so reading it back gives a line of text.
- `bookmark-card`: a bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, so reading the Markdown back gives the link rather than the card.
- `table-of-contents`: a table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block.
- `footnote-orphan`: a footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API.
- `columns`: a columns block is written as its columns one after another, left to right, without their widths. Reading the Markdown back gives those blocks in one column. In a table cell, the columns' blocks become part of the cell's one line, as `table-cell-blocks` describes.
- `citation-text`: a citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it isn't written, so reading the Markdown back gives that text rather than the citation.
- `bibliography`: a bibliography block is written as a heading that names the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them aren't written, so reading the Markdown back gives a heading and a list rather than the note's bibliography.
- `invalid-structure`: malformed table, list or code block structure is simplified.
- `unsupported-mark`: an unknown mark is dropped while its text is kept.
- `unsupported-content`: an unknown node is reduced to its text.
- `exceeds-write-limit`: the markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.

## Folders

A folder files notes for quick access and can sit under one of the user's subjects. `GET /note-folders` lists every folder the key's user owns, by name, then id. `POST /note-folders` creates one, and a duplicate name returns 409 `conflict`. Names are compared exactly, so `Bio` and `bio` are different. An `icon` must be one of the icons the folder picker offers, or the request returns 400 `invalid_request`; a folder stored with an older icon reads back as `Folder`.

`PATCH /note-folders/{folderId}` changes only the fields you send, and `null` on `subjectId` unfiles the folder. Renaming to a name another of the user's folders has returns 409 `conflict`, as creating one does. `DELETE /note-folders/{folderId}` deletes the folder and unfiles its notes rather than deleting them; their content, files and embeddings are untouched.

Folder endpoints reuse the `notes:*` scopes: reading a folder needs `notes:view`, and writing one needs the matching `notes:create`, `notes:update` or `notes:delete`.

## List notes

`GET https://workroad.app/api/v1/notes`

Newest first. Note content is omitted; fetch a single note for content. Trashed notes and templates are not listed. Each note carries its `icon`, its `cover` image URL and its `tags`.

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

### Query parameters

- `limit`: integer, 1 to 100, default `25`. Page size, 1-100.
- `cursor`: string or null, up to 512 characters. Opaque. Pass `nextCursor` from the previous page unchanged.
- `folderId`: `"none"` or string (uuid). `none` for notes outside any folder, or a folder id for that folder's notes.
  - Or `"none"`
  - Or string (uuid)

### Response

`200` OK

- `data`: array of objects, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
- `nextCursor`: string or null, required. Opaque. Pass as `?cursor=` unchanged to fetch the next page. `null` on the last page.

#### Example

```json
{
  "data": [
    {
      "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
      "name": "Photosynthesis",
      "folder": {
        "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
        "name": "Biology",
        "subjectId": null
      },
      "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
      "icon": "🌱",
      "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
      "tags": [
        {
          "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
          "name": "Exam"
        }
      ],
      "createdAt": "2026-01-15T09:30:00.000Z",
      "updatedAt": "2026-02-01T12:00:00.000Z"
    }
  ],
  "nextCursor": null
}
```

### 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 endpoint exists at this path.
- `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

## Create a note

`POST https://workroad.app/api/v1/notes`

Creates a note, optionally inside one of the user's folders and under one of their subjects. Send `markdown` to give it content; without it the note is empty.

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

- MCP tool: `createNote` on `https://workroad.app/api/mcp`
- Required scopes: notes:create
- Test mode: `POST https://workroad.app/api/test/v1/notes` checks the request and returns the example response without changing anything.

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

- `name`: string, required, 1-64 characters
- `folderId`: string (uuid) or null. Must be a folder owned by the key's user.
- `subjectId`: string (uuid) or null. The note's own subject, which wins over its folder's. Must be a subject owned by the key's user (`GET /subjects`); any other id is refused with 400 `unknown-subject`. Omit it to file the note under its folder's subject.
- `markdown`: string, up to 100000 characters. Markdown, up to 100000 characters. Stored as editor JSON, not as Markdown.

### Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
  - `fullWidth`: boolean, required
  - `contentFormat`: `"tiptap"`, required. Current serialization of `content`.
  - `contentUnstable`: `true`, required. Editor-specific TipTap JSON. Node and attribute shapes may change.
  - `content`: any value, required. Editor document, returned verbatim. See `contentUnstable`.
  - `markdown`: string or null, required. The note as markdown. Writing it back with `PUT /notes/{noteId}/content` reproduces the note exactly when `markdownLosses` is empty. Otherwise the listed parts come back simplified. Block ids are not in the markdown; a write mints new ones. An empty image alt or title reads back as null. Null when the note's content isn't an editor document.
  - `markdownLosses`: array of one of `"empty-paragraphs"`, `"collapsed-spaces"`, `"image-size"`, `"table-layout"`, `"table-cell-blocks"`, `"table-header"`, `"table-cell-background"`, `"ordered-list-type"`, `"image-upload-state"`, `"inline-math"`, `"mention-label"`, `"formatting-edges"`, `"highlight"`, `"text-colour"`, `"toggle-summary-formatting"`, `"toggle-heading-level"`, `"code-block-language"`, `"code-block-filename"`, `"code-block-wrap"`, `"block-math"`, `"image-address"`, `"image-loading"`, `"link-address"`, `"link-title"`, `"link-attributes"`, `"mention-target"`, `"date-chip"`, `"heading-level"`, `"ordered-list-start"`, `"callout-kind"`, `"callout-nesting"`, `"columns"`, `"table-of-contents"`, `"footnote-orphan"`, `"youtube-embed"`, `"pdf-embed"`, `"recording-embed"`, `"bookmark-card"`, `"citation-text"`, `"bibliography"`, `"invalid-structure"`, `"unsupported-mark"`, `"unsupported-content"`, `"exceeds-write-limit"`, required. What `markdown` simplifies, each value once. Empty when it reproduces the note exactly. - `empty-paragraphs`: Empty paragraphs are left out. One empty paragraph at the end of the note is not reported, because the editor adds it back. - `collapsed-spaces`: Runs of spaces, tabs and line breaks inside text become one space. Spaces at the start or end of a paragraph, heading or cell, or next to a hard break, are dropped. A hard break at the end of a paragraph is dropped, and one in a heading becomes a space. - `image-size`: Image width and height are left out. - `table-layout`: Merged cells, column widths and cell alignment are left out. - `table-cell-blocks`: A cell holding a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph is written as one line: each paragraph, heading or toggle summary in it on a line of its own, separated by `<br>`, with a formula as inline math and code as its text. List bullets and checkboxes, quote and callout markers, images and embeds in the cell are left out, and reading the Markdown back gives one paragraph with line breaks. Footnote markers, math, citations, dates and mentions are written as anywhere else. - `table-header`: The first row is written as the header even when its cells aren't header cells, and header cells in later rows become normal cells. - `table-cell-background`: Cell background colours are left out. - `ordered-list-type`: Letter and Roman numbering become plain numbers. - `image-upload-state`: An image that never finished uploading is written as its alt text, and its upload details are left out. - `inline-math`: Inline math that `$…$` can't hold is written as text: empty math, latex with an unescaped `$` or a trailing backslash, and math followed directly by a digit. Spaces at either end of the latex are trimmed. - `mention-label`: A mention without a label gets its kind as the label, and line breaks in a label become spaces. Label-only export turns mentions into plain text. - `formatting-edges`: Bold, italic and strikethrough can't start or end on a space, so the space moves outside the formatting. Where they can't start or end at all, such as bold text that starts with punctuation right after a letter, the paragraph's bold, italic and strikethrough are dropped. - `highlight`: Highlighting is dropped while its text is kept. - `text-colour`: Text colour is dropped while the text is kept. - `toggle-summary-formatting`: A toggle's summary is written as plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting. Whether the toggle was open is never stored, so there is nothing to report for it. - `toggle-heading-level`: A toggle whose summary reads as a heading keeps the heading's text and loses its level. The summary is written inside a `<summary>` tag, which carries the text and nothing else. - `code-block-language`: A code block language that cannot fit in a Markdown fence is left out. - `code-block-filename`: A code block's filename is left out. A Markdown fence carries the language and nothing else. - `code-block-wrap`: A code block's soft-wrap setting is left out. It is a view preference, so the code itself is unchanged. - `block-math`: A formula line containing only $$ is changed to $ $. - `image-address`: An image address is normalized, or the image becomes alt text when its address is refused. - `image-loading`: The image loading preference is reset to lazy. - `link-address`: A link address is normalized, or the link becomes text when its address is refused. - `link-title`: A numeric or boolean link title is read back as that value instead of text. - `link-attributes`: The link target, rel or CSS class is reset to the editor default. - `mention-target`: A mention with an invalid kind or ID becomes plain text. - `date-chip`: An inline date is written as its day in `YYYY-MM-DD` form, as plain text. Reading the Markdown back gives that text rather than the chip. - `heading-level`: An unsupported heading level becomes level 1 or 4. - `ordered-list-start`: A list start that Markdown cannot represent becomes 1. - `callout-kind`: A callout of an unknown kind is written as a note. - `callout-nesting`: A callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind. - `columns`: Markdown has no columns, so a columns block is written as its columns one after another, left to right, and the widths are left out. Each block in them is written as it would be on its own, in a quote or a toggle too, and reading the Markdown back gives those blocks in one column. A columns block in a table cell is written as part of the cell's one line instead, as `table-cell-blocks` describes. - `table-of-contents`: A table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block. - `footnote-orphan`: A footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API. - `youtube-embed`: A YouTube block is written as a link to the video. Reading the Markdown back gives the link, not the player. - `pdf-embed`: A PDF block is written as a link to the file. Reading the Markdown back gives the link, not the viewer. - `recording-embed`: A recording's audio player is written as its label. The recording itself is not part of the Markdown, so reading it back gives a line of text. - `bookmark-card`: A bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, and reading the Markdown back gives the link rather than the card. - `citation-text`: A citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it is not written, so reading the Markdown back gives that text rather than the citation. - `bibliography`: A bibliography is written as a heading naming the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them are not written, so reading the Markdown back gives a heading and a list rather than the note's bibliography. - `invalid-structure`: Malformed table, list or code block structure is simplified. - `unsupported-mark`: An unknown mark is dropped while its text is kept. - `unsupported-content`: An unknown node is reduced to its text. - `exceeds-write-limit`: The markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.
  - `version`: integer, required. Increases on every content change. Send it as `baseVersion` to avoid overwriting a newer save.
  - `pinned`: boolean, required. Pinned on the notes home.
  - `summary`: object or null, required
    - `summary`: string, required
    - `keywords`: array of string, required
    - `createdAt`: string (date-time), required. When the summary was generated.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "name": "Photosynthesis",
    "folder": {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null
    },
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "icon": "🌱",
    "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
    "tags": [
      {
        "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
        "name": "Exam"
      }
    ],
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "fullWidth": false,
    "contentFormat": "tiptap",
    "contentUnstable": true,
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"
          },
          "content": [
            {
              "type": "text",
              "text": "Light becomes chemical energy."
            }
          ]
        }
      ]
    },
    "markdown": "Light becomes chemical energy.",
    "markdownLosses": [],
    "version": 3,
    "pinned": false,
    "summary": {
      "summary": "How plants turn light into sugar.",
      "keywords": [
        "chlorophyll",
        "glucose",
        "light reactions"
      ],
      "createdAt": "2026-02-01T12:00:00.000Z"
    }
  }
}
```

### Errors

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

- `400`: Invalid body, a `folderId` that is not one of the user's folders (`details.reason` is `unknown-folder`), or a `subjectId` that is not one of their subjects (`details.reason` is `unknown-subject`).
- `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 endpoint exists at this path.
- `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

## Get a note with content

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

Returns a note with its editor content and the same content as Markdown.

`subjectId` is the note's own subject when set; otherwise it inherits its folder's subject. `folder` is `null` when the note is not in a folder. See the response schema for the remaining fields.

- MCP tool: `getNote` on `https://workroad.app/api/mcp`
- Required scopes: notes:view
- Test mode: `GET https://workroad.app/api/test/v1/notes/{noteId}` 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`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
  - `fullWidth`: boolean, required
  - `contentFormat`: `"tiptap"`, required. Current serialization of `content`.
  - `contentUnstable`: `true`, required. Editor-specific TipTap JSON. Node and attribute shapes may change.
  - `content`: any value, required. Editor document, returned verbatim. See `contentUnstable`.
  - `markdown`: string or null, required. The note as markdown. Writing it back with `PUT /notes/{noteId}/content` reproduces the note exactly when `markdownLosses` is empty. Otherwise the listed parts come back simplified. Block ids are not in the markdown; a write mints new ones. An empty image alt or title reads back as null. Null when the note's content isn't an editor document.
  - `markdownLosses`: array of one of `"empty-paragraphs"`, `"collapsed-spaces"`, `"image-size"`, `"table-layout"`, `"table-cell-blocks"`, `"table-header"`, `"table-cell-background"`, `"ordered-list-type"`, `"image-upload-state"`, `"inline-math"`, `"mention-label"`, `"formatting-edges"`, `"highlight"`, `"text-colour"`, `"toggle-summary-formatting"`, `"toggle-heading-level"`, `"code-block-language"`, `"code-block-filename"`, `"code-block-wrap"`, `"block-math"`, `"image-address"`, `"image-loading"`, `"link-address"`, `"link-title"`, `"link-attributes"`, `"mention-target"`, `"date-chip"`, `"heading-level"`, `"ordered-list-start"`, `"callout-kind"`, `"callout-nesting"`, `"columns"`, `"table-of-contents"`, `"footnote-orphan"`, `"youtube-embed"`, `"pdf-embed"`, `"recording-embed"`, `"bookmark-card"`, `"citation-text"`, `"bibliography"`, `"invalid-structure"`, `"unsupported-mark"`, `"unsupported-content"`, `"exceeds-write-limit"`, required. What `markdown` simplifies, each value once. Empty when it reproduces the note exactly. - `empty-paragraphs`: Empty paragraphs are left out. One empty paragraph at the end of the note is not reported, because the editor adds it back. - `collapsed-spaces`: Runs of spaces, tabs and line breaks inside text become one space. Spaces at the start or end of a paragraph, heading or cell, or next to a hard break, are dropped. A hard break at the end of a paragraph is dropped, and one in a heading becomes a space. - `image-size`: Image width and height are left out. - `table-layout`: Merged cells, column widths and cell alignment are left out. - `table-cell-blocks`: A cell holding a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph is written as one line: each paragraph, heading or toggle summary in it on a line of its own, separated by `<br>`, with a formula as inline math and code as its text. List bullets and checkboxes, quote and callout markers, images and embeds in the cell are left out, and reading the Markdown back gives one paragraph with line breaks. Footnote markers, math, citations, dates and mentions are written as anywhere else. - `table-header`: The first row is written as the header even when its cells aren't header cells, and header cells in later rows become normal cells. - `table-cell-background`: Cell background colours are left out. - `ordered-list-type`: Letter and Roman numbering become plain numbers. - `image-upload-state`: An image that never finished uploading is written as its alt text, and its upload details are left out. - `inline-math`: Inline math that `$…$` can't hold is written as text: empty math, latex with an unescaped `$` or a trailing backslash, and math followed directly by a digit. Spaces at either end of the latex are trimmed. - `mention-label`: A mention without a label gets its kind as the label, and line breaks in a label become spaces. Label-only export turns mentions into plain text. - `formatting-edges`: Bold, italic and strikethrough can't start or end on a space, so the space moves outside the formatting. Where they can't start or end at all, such as bold text that starts with punctuation right after a letter, the paragraph's bold, italic and strikethrough are dropped. - `highlight`: Highlighting is dropped while its text is kept. - `text-colour`: Text colour is dropped while the text is kept. - `toggle-summary-formatting`: A toggle's summary is written as plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting. Whether the toggle was open is never stored, so there is nothing to report for it. - `toggle-heading-level`: A toggle whose summary reads as a heading keeps the heading's text and loses its level. The summary is written inside a `<summary>` tag, which carries the text and nothing else. - `code-block-language`: A code block language that cannot fit in a Markdown fence is left out. - `code-block-filename`: A code block's filename is left out. A Markdown fence carries the language and nothing else. - `code-block-wrap`: A code block's soft-wrap setting is left out. It is a view preference, so the code itself is unchanged. - `block-math`: A formula line containing only $$ is changed to $ $. - `image-address`: An image address is normalized, or the image becomes alt text when its address is refused. - `image-loading`: The image loading preference is reset to lazy. - `link-address`: A link address is normalized, or the link becomes text when its address is refused. - `link-title`: A numeric or boolean link title is read back as that value instead of text. - `link-attributes`: The link target, rel or CSS class is reset to the editor default. - `mention-target`: A mention with an invalid kind or ID becomes plain text. - `date-chip`: An inline date is written as its day in `YYYY-MM-DD` form, as plain text. Reading the Markdown back gives that text rather than the chip. - `heading-level`: An unsupported heading level becomes level 1 or 4. - `ordered-list-start`: A list start that Markdown cannot represent becomes 1. - `callout-kind`: A callout of an unknown kind is written as a note. - `callout-nesting`: A callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind. - `columns`: Markdown has no columns, so a columns block is written as its columns one after another, left to right, and the widths are left out. Each block in them is written as it would be on its own, in a quote or a toggle too, and reading the Markdown back gives those blocks in one column. A columns block in a table cell is written as part of the cell's one line instead, as `table-cell-blocks` describes. - `table-of-contents`: A table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block. - `footnote-orphan`: A footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API. - `youtube-embed`: A YouTube block is written as a link to the video. Reading the Markdown back gives the link, not the player. - `pdf-embed`: A PDF block is written as a link to the file. Reading the Markdown back gives the link, not the viewer. - `recording-embed`: A recording's audio player is written as its label. The recording itself is not part of the Markdown, so reading it back gives a line of text. - `bookmark-card`: A bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, and reading the Markdown back gives the link rather than the card. - `citation-text`: A citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it is not written, so reading the Markdown back gives that text rather than the citation. - `bibliography`: A bibliography is written as a heading naming the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them are not written, so reading the Markdown back gives a heading and a list rather than the note's bibliography. - `invalid-structure`: Malformed table, list or code block structure is simplified. - `unsupported-mark`: An unknown mark is dropped while its text is kept. - `unsupported-content`: An unknown node is reduced to its text. - `exceeds-write-limit`: The markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.
  - `version`: integer, required. Increases on every content change. Send it as `baseVersion` to avoid overwriting a newer save.
  - `pinned`: boolean, required. Pinned on the notes home.
  - `summary`: object or null, required
    - `summary`: string, required
    - `keywords`: array of string, required
    - `createdAt`: string (date-time), required. When the summary was generated.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "name": "Photosynthesis",
    "folder": {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null
    },
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "icon": "🌱",
    "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
    "tags": [
      {
        "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
        "name": "Exam"
      }
    ],
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "fullWidth": false,
    "contentFormat": "tiptap",
    "contentUnstable": true,
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"
          },
          "content": [
            {
              "type": "text",
              "text": "Light becomes chemical energy."
            }
          ]
        }
      ]
    },
    "markdown": "Light becomes chemical energy.",
    "markdownLosses": [],
    "version": 3,
    "pinned": false,
    "summary": {
      "summary": "How plants turn light into sugar.",
      "keywords": [
        "chlorophyll",
        "glucose",
        "light reactions"
      ],
      "createdAt": "2026-02-01T12:00: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 resource 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

## Update a note

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

Renames the note, moves it into or out of a folder, files it under a subject, pins it, or sets its `icon`. Only the fields you send change. Content is written with `PUT /notes/{noteId}/content`. The cover image and the tags are read here but set in the app.

- MCP tool: `updateNote` on `https://workroad.app/api/mcp`
- Required scopes: notes:update
- Test mode: `PATCH https://workroad.app/api/test/v1/notes/{noteId}` 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.

### Request body

JSON.

- `name`: string, 1-64 characters
- `folderId`: string (uuid) or null. A folder owned by the key's user. `null` takes the note out of its folder.
- `pinned`: boolean. Pins the note on the notes home. Templates cannot be pinned.
- `subjectId`: string (uuid) or null. The note's own subject, which wins over its folder's. Must be a subject owned by the key's user (`GET /subjects`); any other id is refused with 400 `unknown-subject`. `null` clears it, so the note takes its folder's subject again.
- `icon`: string or null. One emoji shown beside the note's name, such as `📘` or `🇫🇷`: a single emoji, not text or several emoji. `null` removes it. The note's cover image and its tags are set in the app, not through this endpoint.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
  - `fullWidth`: boolean, required
  - `contentFormat`: `"tiptap"`, required. Current serialization of `content`.
  - `contentUnstable`: `true`, required. Editor-specific TipTap JSON. Node and attribute shapes may change.
  - `content`: any value, required. Editor document, returned verbatim. See `contentUnstable`.
  - `markdown`: string or null, required. The note as markdown. Writing it back with `PUT /notes/{noteId}/content` reproduces the note exactly when `markdownLosses` is empty. Otherwise the listed parts come back simplified. Block ids are not in the markdown; a write mints new ones. An empty image alt or title reads back as null. Null when the note's content isn't an editor document.
  - `markdownLosses`: array of one of `"empty-paragraphs"`, `"collapsed-spaces"`, `"image-size"`, `"table-layout"`, `"table-cell-blocks"`, `"table-header"`, `"table-cell-background"`, `"ordered-list-type"`, `"image-upload-state"`, `"inline-math"`, `"mention-label"`, `"formatting-edges"`, `"highlight"`, `"text-colour"`, `"toggle-summary-formatting"`, `"toggle-heading-level"`, `"code-block-language"`, `"code-block-filename"`, `"code-block-wrap"`, `"block-math"`, `"image-address"`, `"image-loading"`, `"link-address"`, `"link-title"`, `"link-attributes"`, `"mention-target"`, `"date-chip"`, `"heading-level"`, `"ordered-list-start"`, `"callout-kind"`, `"callout-nesting"`, `"columns"`, `"table-of-contents"`, `"footnote-orphan"`, `"youtube-embed"`, `"pdf-embed"`, `"recording-embed"`, `"bookmark-card"`, `"citation-text"`, `"bibliography"`, `"invalid-structure"`, `"unsupported-mark"`, `"unsupported-content"`, `"exceeds-write-limit"`, required. What `markdown` simplifies, each value once. Empty when it reproduces the note exactly. - `empty-paragraphs`: Empty paragraphs are left out. One empty paragraph at the end of the note is not reported, because the editor adds it back. - `collapsed-spaces`: Runs of spaces, tabs and line breaks inside text become one space. Spaces at the start or end of a paragraph, heading or cell, or next to a hard break, are dropped. A hard break at the end of a paragraph is dropped, and one in a heading becomes a space. - `image-size`: Image width and height are left out. - `table-layout`: Merged cells, column widths and cell alignment are left out. - `table-cell-blocks`: A cell holding a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph is written as one line: each paragraph, heading or toggle summary in it on a line of its own, separated by `<br>`, with a formula as inline math and code as its text. List bullets and checkboxes, quote and callout markers, images and embeds in the cell are left out, and reading the Markdown back gives one paragraph with line breaks. Footnote markers, math, citations, dates and mentions are written as anywhere else. - `table-header`: The first row is written as the header even when its cells aren't header cells, and header cells in later rows become normal cells. - `table-cell-background`: Cell background colours are left out. - `ordered-list-type`: Letter and Roman numbering become plain numbers. - `image-upload-state`: An image that never finished uploading is written as its alt text, and its upload details are left out. - `inline-math`: Inline math that `$…$` can't hold is written as text: empty math, latex with an unescaped `$` or a trailing backslash, and math followed directly by a digit. Spaces at either end of the latex are trimmed. - `mention-label`: A mention without a label gets its kind as the label, and line breaks in a label become spaces. Label-only export turns mentions into plain text. - `formatting-edges`: Bold, italic and strikethrough can't start or end on a space, so the space moves outside the formatting. Where they can't start or end at all, such as bold text that starts with punctuation right after a letter, the paragraph's bold, italic and strikethrough are dropped. - `highlight`: Highlighting is dropped while its text is kept. - `text-colour`: Text colour is dropped while the text is kept. - `toggle-summary-formatting`: A toggle's summary is written as plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting. Whether the toggle was open is never stored, so there is nothing to report for it. - `toggle-heading-level`: A toggle whose summary reads as a heading keeps the heading's text and loses its level. The summary is written inside a `<summary>` tag, which carries the text and nothing else. - `code-block-language`: A code block language that cannot fit in a Markdown fence is left out. - `code-block-filename`: A code block's filename is left out. A Markdown fence carries the language and nothing else. - `code-block-wrap`: A code block's soft-wrap setting is left out. It is a view preference, so the code itself is unchanged. - `block-math`: A formula line containing only $$ is changed to $ $. - `image-address`: An image address is normalized, or the image becomes alt text when its address is refused. - `image-loading`: The image loading preference is reset to lazy. - `link-address`: A link address is normalized, or the link becomes text when its address is refused. - `link-title`: A numeric or boolean link title is read back as that value instead of text. - `link-attributes`: The link target, rel or CSS class is reset to the editor default. - `mention-target`: A mention with an invalid kind or ID becomes plain text. - `date-chip`: An inline date is written as its day in `YYYY-MM-DD` form, as plain text. Reading the Markdown back gives that text rather than the chip. - `heading-level`: An unsupported heading level becomes level 1 or 4. - `ordered-list-start`: A list start that Markdown cannot represent becomes 1. - `callout-kind`: A callout of an unknown kind is written as a note. - `callout-nesting`: A callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind. - `columns`: Markdown has no columns, so a columns block is written as its columns one after another, left to right, and the widths are left out. Each block in them is written as it would be on its own, in a quote or a toggle too, and reading the Markdown back gives those blocks in one column. A columns block in a table cell is written as part of the cell's one line instead, as `table-cell-blocks` describes. - `table-of-contents`: A table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block. - `footnote-orphan`: A footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API. - `youtube-embed`: A YouTube block is written as a link to the video. Reading the Markdown back gives the link, not the player. - `pdf-embed`: A PDF block is written as a link to the file. Reading the Markdown back gives the link, not the viewer. - `recording-embed`: A recording's audio player is written as its label. The recording itself is not part of the Markdown, so reading it back gives a line of text. - `bookmark-card`: A bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, and reading the Markdown back gives the link rather than the card. - `citation-text`: A citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it is not written, so reading the Markdown back gives that text rather than the citation. - `bibliography`: A bibliography is written as a heading naming the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them are not written, so reading the Markdown back gives a heading and a list rather than the note's bibliography. - `invalid-structure`: Malformed table, list or code block structure is simplified. - `unsupported-mark`: An unknown mark is dropped while its text is kept. - `unsupported-content`: An unknown node is reduced to its text. - `exceeds-write-limit`: The markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.
  - `version`: integer, required. Increases on every content change. Send it as `baseVersion` to avoid overwriting a newer save.
  - `pinned`: boolean, required. Pinned on the notes home.
  - `summary`: object or null, required
    - `summary`: string, required
    - `keywords`: array of string, required
    - `createdAt`: string (date-time), required. When the summary was generated.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "name": "Photosynthesis",
    "folder": {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null
    },
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "icon": "🌱",
    "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
    "tags": [
      {
        "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
        "name": "Exam"
      }
    ],
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "fullWidth": false,
    "contentFormat": "tiptap",
    "contentUnstable": true,
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"
          },
          "content": [
            {
              "type": "text",
              "text": "Light becomes chemical energy."
            }
          ]
        }
      ]
    },
    "markdown": "Light becomes chemical energy.",
    "markdownLosses": [],
    "version": 3,
    "pinned": false,
    "summary": {
      "summary": "How plants turn light into sugar.",
      "keywords": [
        "chlorophyll",
        "glucose",
        "light reactions"
      ],
      "createdAt": "2026-02-01T12:00:00.000Z"
    }
  }
}
```

### Errors

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

- `400`: Invalid body, no field to update, a `folderId` that is not the user's (`details.reason` is `unknown-folder`), a `subjectId` that is not the user's (`details.reason` is `unknown-subject`), or `pinned` sent for a template (`details.reason` is `template-note`).
- `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

## Write note content as Markdown

`PUT https://workroad.app/api/v1/notes/{noteId}/content`

Converts `markdown` (up to 100000 characters) to the editor format and replaces the note's content with it. To add blocks after the existing ones, use `POST /notes/{noteId}/content/append`. A replace keeps the inline images that are still in the Markdown, written as `![alt](src)` the way `GET /notes/{noteId}` returns them, and deletes the ones it leaves out. An image whose upload the web editor hasn't confirmed yet can still be deleted 24 hours after the upload. Send `baseVersion` to write only if nobody saved since you read the note.

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

- MCP tool: `replaceNoteContent` on `https://workroad.app/api/mcp`
- Required scopes: notes:update
- Test mode: `PUT https://workroad.app/api/test/v1/notes/{noteId}/content` 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.

### Request body

JSON.

- `markdown`: string, required, up to 100000 characters. Markdown, up to 100000 characters. Stored as editor JSON, not as Markdown.
- `baseVersion`: integer, at least 0. The `version` you last read. When the note has changed since, nothing is written and the response is 409. Omit it to write regardless.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
  - `fullWidth`: boolean, required
  - `contentFormat`: `"tiptap"`, required. Current serialization of `content`.
  - `contentUnstable`: `true`, required. Editor-specific TipTap JSON. Node and attribute shapes may change.
  - `content`: any value, required. Editor document, returned verbatim. See `contentUnstable`.
  - `markdown`: string or null, required. The note as markdown. Writing it back with `PUT /notes/{noteId}/content` reproduces the note exactly when `markdownLosses` is empty. Otherwise the listed parts come back simplified. Block ids are not in the markdown; a write mints new ones. An empty image alt or title reads back as null. Null when the note's content isn't an editor document.
  - `markdownLosses`: array of one of `"empty-paragraphs"`, `"collapsed-spaces"`, `"image-size"`, `"table-layout"`, `"table-cell-blocks"`, `"table-header"`, `"table-cell-background"`, `"ordered-list-type"`, `"image-upload-state"`, `"inline-math"`, `"mention-label"`, `"formatting-edges"`, `"highlight"`, `"text-colour"`, `"toggle-summary-formatting"`, `"toggle-heading-level"`, `"code-block-language"`, `"code-block-filename"`, `"code-block-wrap"`, `"block-math"`, `"image-address"`, `"image-loading"`, `"link-address"`, `"link-title"`, `"link-attributes"`, `"mention-target"`, `"date-chip"`, `"heading-level"`, `"ordered-list-start"`, `"callout-kind"`, `"callout-nesting"`, `"columns"`, `"table-of-contents"`, `"footnote-orphan"`, `"youtube-embed"`, `"pdf-embed"`, `"recording-embed"`, `"bookmark-card"`, `"citation-text"`, `"bibliography"`, `"invalid-structure"`, `"unsupported-mark"`, `"unsupported-content"`, `"exceeds-write-limit"`, required. What `markdown` simplifies, each value once. Empty when it reproduces the note exactly. - `empty-paragraphs`: Empty paragraphs are left out. One empty paragraph at the end of the note is not reported, because the editor adds it back. - `collapsed-spaces`: Runs of spaces, tabs and line breaks inside text become one space. Spaces at the start or end of a paragraph, heading or cell, or next to a hard break, are dropped. A hard break at the end of a paragraph is dropped, and one in a heading becomes a space. - `image-size`: Image width and height are left out. - `table-layout`: Merged cells, column widths and cell alignment are left out. - `table-cell-blocks`: A cell holding a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph is written as one line: each paragraph, heading or toggle summary in it on a line of its own, separated by `<br>`, with a formula as inline math and code as its text. List bullets and checkboxes, quote and callout markers, images and embeds in the cell are left out, and reading the Markdown back gives one paragraph with line breaks. Footnote markers, math, citations, dates and mentions are written as anywhere else. - `table-header`: The first row is written as the header even when its cells aren't header cells, and header cells in later rows become normal cells. - `table-cell-background`: Cell background colours are left out. - `ordered-list-type`: Letter and Roman numbering become plain numbers. - `image-upload-state`: An image that never finished uploading is written as its alt text, and its upload details are left out. - `inline-math`: Inline math that `$…$` can't hold is written as text: empty math, latex with an unescaped `$` or a trailing backslash, and math followed directly by a digit. Spaces at either end of the latex are trimmed. - `mention-label`: A mention without a label gets its kind as the label, and line breaks in a label become spaces. Label-only export turns mentions into plain text. - `formatting-edges`: Bold, italic and strikethrough can't start or end on a space, so the space moves outside the formatting. Where they can't start or end at all, such as bold text that starts with punctuation right after a letter, the paragraph's bold, italic and strikethrough are dropped. - `highlight`: Highlighting is dropped while its text is kept. - `text-colour`: Text colour is dropped while the text is kept. - `toggle-summary-formatting`: A toggle's summary is written as plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting. Whether the toggle was open is never stored, so there is nothing to report for it. - `toggle-heading-level`: A toggle whose summary reads as a heading keeps the heading's text and loses its level. The summary is written inside a `<summary>` tag, which carries the text and nothing else. - `code-block-language`: A code block language that cannot fit in a Markdown fence is left out. - `code-block-filename`: A code block's filename is left out. A Markdown fence carries the language and nothing else. - `code-block-wrap`: A code block's soft-wrap setting is left out. It is a view preference, so the code itself is unchanged. - `block-math`: A formula line containing only $$ is changed to $ $. - `image-address`: An image address is normalized, or the image becomes alt text when its address is refused. - `image-loading`: The image loading preference is reset to lazy. - `link-address`: A link address is normalized, or the link becomes text when its address is refused. - `link-title`: A numeric or boolean link title is read back as that value instead of text. - `link-attributes`: The link target, rel or CSS class is reset to the editor default. - `mention-target`: A mention with an invalid kind or ID becomes plain text. - `date-chip`: An inline date is written as its day in `YYYY-MM-DD` form, as plain text. Reading the Markdown back gives that text rather than the chip. - `heading-level`: An unsupported heading level becomes level 1 or 4. - `ordered-list-start`: A list start that Markdown cannot represent becomes 1. - `callout-kind`: A callout of an unknown kind is written as a note. - `callout-nesting`: A callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind. - `columns`: Markdown has no columns, so a columns block is written as its columns one after another, left to right, and the widths are left out. Each block in them is written as it would be on its own, in a quote or a toggle too, and reading the Markdown back gives those blocks in one column. A columns block in a table cell is written as part of the cell's one line instead, as `table-cell-blocks` describes. - `table-of-contents`: A table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block. - `footnote-orphan`: A footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API. - `youtube-embed`: A YouTube block is written as a link to the video. Reading the Markdown back gives the link, not the player. - `pdf-embed`: A PDF block is written as a link to the file. Reading the Markdown back gives the link, not the viewer. - `recording-embed`: A recording's audio player is written as its label. The recording itself is not part of the Markdown, so reading it back gives a line of text. - `bookmark-card`: A bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, and reading the Markdown back gives the link rather than the card. - `citation-text`: A citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it is not written, so reading the Markdown back gives that text rather than the citation. - `bibliography`: A bibliography is written as a heading naming the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them are not written, so reading the Markdown back gives a heading and a list rather than the note's bibliography. - `invalid-structure`: Malformed table, list or code block structure is simplified. - `unsupported-mark`: An unknown mark is dropped while its text is kept. - `unsupported-content`: An unknown node is reduced to its text. - `exceeds-write-limit`: The markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.
  - `version`: integer, required. Increases on every content change. Send it as `baseVersion` to avoid overwriting a newer save.
  - `pinned`: boolean, required. Pinned on the notes home.
  - `summary`: object or null, required
    - `summary`: string, required
    - `keywords`: array of string, required
    - `createdAt`: string (date-time), required. When the summary was generated.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "name": "Photosynthesis",
    "folder": {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null
    },
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "icon": "🌱",
    "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
    "tags": [
      {
        "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
        "name": "Exam"
      }
    ],
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "fullWidth": false,
    "contentFormat": "tiptap",
    "contentUnstable": true,
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"
          },
          "content": [
            {
              "type": "text",
              "text": "Light becomes chemical energy."
            }
          ]
        }
      ]
    },
    "markdown": "Light becomes chemical energy.",
    "markdownLosses": [],
    "version": 3,
    "pinned": false,
    "summary": {
      "summary": "How plants turn light into sugar.",
      "keywords": [
        "chlorophyll",
        "glucose",
        "light reactions"
      ],
      "createdAt": "2026-02-01T12:00:00.000Z"
    }
  }
}
```

### Errors

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

- `400`: Invalid body.
- `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`: The note changed since `baseVersion`. Nothing was written. `details` holds `baseVersion` and `currentVersion`.
- `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

## Append Markdown to a note

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

Converts `markdown` (up to 100000 characters) to the editor format and adds it after the note's existing blocks. Send `Idempotency-Key` so a retry can't append twice. Sending `baseVersion` too makes a retry without a key fail with 409 instead of appending again.

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

- MCP tool: `appendNoteContent` on `https://workroad.app/api/mcp`
- Required scopes: notes:update
- Test mode: `POST https://workroad.app/api/test/v1/notes/{noteId}/content/append` 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.

- `markdown`: string, required, up to 100000 characters. Markdown, up to 100000 characters. Stored as editor JSON, not as Markdown.
- `baseVersion`: integer, at least 0. The `version` you last read. When the note has changed since, nothing is written and the response is 409. Omit it to write regardless.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
  - `fullWidth`: boolean, required
  - `contentFormat`: `"tiptap"`, required. Current serialization of `content`.
  - `contentUnstable`: `true`, required. Editor-specific TipTap JSON. Node and attribute shapes may change.
  - `content`: any value, required. Editor document, returned verbatim. See `contentUnstable`.
  - `markdown`: string or null, required. The note as markdown. Writing it back with `PUT /notes/{noteId}/content` reproduces the note exactly when `markdownLosses` is empty. Otherwise the listed parts come back simplified. Block ids are not in the markdown; a write mints new ones. An empty image alt or title reads back as null. Null when the note's content isn't an editor document.
  - `markdownLosses`: array of one of `"empty-paragraphs"`, `"collapsed-spaces"`, `"image-size"`, `"table-layout"`, `"table-cell-blocks"`, `"table-header"`, `"table-cell-background"`, `"ordered-list-type"`, `"image-upload-state"`, `"inline-math"`, `"mention-label"`, `"formatting-edges"`, `"highlight"`, `"text-colour"`, `"toggle-summary-formatting"`, `"toggle-heading-level"`, `"code-block-language"`, `"code-block-filename"`, `"code-block-wrap"`, `"block-math"`, `"image-address"`, `"image-loading"`, `"link-address"`, `"link-title"`, `"link-attributes"`, `"mention-target"`, `"date-chip"`, `"heading-level"`, `"ordered-list-start"`, `"callout-kind"`, `"callout-nesting"`, `"columns"`, `"table-of-contents"`, `"footnote-orphan"`, `"youtube-embed"`, `"pdf-embed"`, `"recording-embed"`, `"bookmark-card"`, `"citation-text"`, `"bibliography"`, `"invalid-structure"`, `"unsupported-mark"`, `"unsupported-content"`, `"exceeds-write-limit"`, required. What `markdown` simplifies, each value once. Empty when it reproduces the note exactly. - `empty-paragraphs`: Empty paragraphs are left out. One empty paragraph at the end of the note is not reported, because the editor adds it back. - `collapsed-spaces`: Runs of spaces, tabs and line breaks inside text become one space. Spaces at the start or end of a paragraph, heading or cell, or next to a hard break, are dropped. A hard break at the end of a paragraph is dropped, and one in a heading becomes a space. - `image-size`: Image width and height are left out. - `table-layout`: Merged cells, column widths and cell alignment are left out. - `table-cell-blocks`: A cell holding a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph is written as one line: each paragraph, heading or toggle summary in it on a line of its own, separated by `<br>`, with a formula as inline math and code as its text. List bullets and checkboxes, quote and callout markers, images and embeds in the cell are left out, and reading the Markdown back gives one paragraph with line breaks. Footnote markers, math, citations, dates and mentions are written as anywhere else. - `table-header`: The first row is written as the header even when its cells aren't header cells, and header cells in later rows become normal cells. - `table-cell-background`: Cell background colours are left out. - `ordered-list-type`: Letter and Roman numbering become plain numbers. - `image-upload-state`: An image that never finished uploading is written as its alt text, and its upload details are left out. - `inline-math`: Inline math that `$…$` can't hold is written as text: empty math, latex with an unescaped `$` or a trailing backslash, and math followed directly by a digit. Spaces at either end of the latex are trimmed. - `mention-label`: A mention without a label gets its kind as the label, and line breaks in a label become spaces. Label-only export turns mentions into plain text. - `formatting-edges`: Bold, italic and strikethrough can't start or end on a space, so the space moves outside the formatting. Where they can't start or end at all, such as bold text that starts with punctuation right after a letter, the paragraph's bold, italic and strikethrough are dropped. - `highlight`: Highlighting is dropped while its text is kept. - `text-colour`: Text colour is dropped while the text is kept. - `toggle-summary-formatting`: A toggle's summary is written as plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting. Whether the toggle was open is never stored, so there is nothing to report for it. - `toggle-heading-level`: A toggle whose summary reads as a heading keeps the heading's text and loses its level. The summary is written inside a `<summary>` tag, which carries the text and nothing else. - `code-block-language`: A code block language that cannot fit in a Markdown fence is left out. - `code-block-filename`: A code block's filename is left out. A Markdown fence carries the language and nothing else. - `code-block-wrap`: A code block's soft-wrap setting is left out. It is a view preference, so the code itself is unchanged. - `block-math`: A formula line containing only $$ is changed to $ $. - `image-address`: An image address is normalized, or the image becomes alt text when its address is refused. - `image-loading`: The image loading preference is reset to lazy. - `link-address`: A link address is normalized, or the link becomes text when its address is refused. - `link-title`: A numeric or boolean link title is read back as that value instead of text. - `link-attributes`: The link target, rel or CSS class is reset to the editor default. - `mention-target`: A mention with an invalid kind or ID becomes plain text. - `date-chip`: An inline date is written as its day in `YYYY-MM-DD` form, as plain text. Reading the Markdown back gives that text rather than the chip. - `heading-level`: An unsupported heading level becomes level 1 or 4. - `ordered-list-start`: A list start that Markdown cannot represent becomes 1. - `callout-kind`: A callout of an unknown kind is written as a note. - `callout-nesting`: A callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind. - `columns`: Markdown has no columns, so a columns block is written as its columns one after another, left to right, and the widths are left out. Each block in them is written as it would be on its own, in a quote or a toggle too, and reading the Markdown back gives those blocks in one column. A columns block in a table cell is written as part of the cell's one line instead, as `table-cell-blocks` describes. - `table-of-contents`: A table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block. - `footnote-orphan`: A footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API. - `youtube-embed`: A YouTube block is written as a link to the video. Reading the Markdown back gives the link, not the player. - `pdf-embed`: A PDF block is written as a link to the file. Reading the Markdown back gives the link, not the viewer. - `recording-embed`: A recording's audio player is written as its label. The recording itself is not part of the Markdown, so reading it back gives a line of text. - `bookmark-card`: A bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, and reading the Markdown back gives the link rather than the card. - `citation-text`: A citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it is not written, so reading the Markdown back gives that text rather than the citation. - `bibliography`: A bibliography is written as a heading naming the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them are not written, so reading the Markdown back gives a heading and a list rather than the note's bibliography. - `invalid-structure`: Malformed table, list or code block structure is simplified. - `unsupported-mark`: An unknown mark is dropped while its text is kept. - `unsupported-content`: An unknown node is reduced to its text. - `exceeds-write-limit`: The markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.
  - `version`: integer, required. Increases on every content change. Send it as `baseVersion` to avoid overwriting a newer save.
  - `pinned`: boolean, required. Pinned on the notes home.
  - `summary`: object or null, required
    - `summary`: string, required
    - `keywords`: array of string, required
    - `createdAt`: string (date-time), required. When the summary was generated.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "name": "Photosynthesis",
    "folder": {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null
    },
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "icon": "🌱",
    "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
    "tags": [
      {
        "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
        "name": "Exam"
      }
    ],
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "fullWidth": false,
    "contentFormat": "tiptap",
    "contentUnstable": true,
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"
          },
          "content": [
            {
              "type": "text",
              "text": "Light becomes chemical energy."
            }
          ]
        }
      ]
    },
    "markdown": "Light becomes chemical energy.",
    "markdownLosses": [],
    "version": 3,
    "pinned": false,
    "summary": {
      "summary": "How plants turn light into sugar.",
      "keywords": [
        "chlorophyll",
        "glucose",
        "light reactions"
      ],
      "createdAt": "2026-02-01T12:00:00.000Z"
    }
  }
}
```

### Errors

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

- `400`: Invalid body, the note's content is not an editor document (`details.field` is `content`, `details.reason` is `legacy-content`), the `markdown` is empty or only whitespace (`details.field` is `markdown`, `details.reason` is `empty-append`), or the append would take the note past 200,000 visible characters (`details.field` is `markdown`, `details.reason` is `note-too-large`).
- `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`: The note changed since `baseVersion`. Nothing was written. `details` holds `baseVersion` and `currentVersion`. 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

## Move a note to the trash

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

The note disappears from every read and stops being searchable. Its files stay, and `POST /notes/{noteId}/restore` brings it back. The trash is emptied after 30 days.

- MCP tool: `deleteNote` on `https://workroad.app/api/mcp`
- Required scopes: notes:delete
- Test mode: `DELETE https://workroad.app/api/test/v1/notes/{noteId}` checks the request and returns the example response without changing anything.
- Next: `restoreNote` brings it back from the trash.

### Path parameters

- `noteId`: string (uuid), required. Note 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-4f3e2d1c0b9a",
    "deleted": true
  }
}
```

### 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

## Restore a note from the trash

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

Brings a trashed note back and returns it. Needs `notes:delete`, the scope that trashed it. Restoration succeeds even above the file or storage limit. Restored files count toward the limits on the next upload.

- MCP tool: `restoreNote` on `https://workroad.app/api/mcp`
- Required scopes: notes:delete
- Test mode: `POST https://workroad.app/api/test/v1/notes/{noteId}/restore` 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.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `folder`: object or null, required
    - `id`: string (uuid), required
    - `name`: string, required
    - `subjectId`: string or null, required. Subject id, when set.
  - `subjectId`: string or null, required. The note's subject id: its own when it has one, otherwise its folder's. `null` when neither has a subject.
  - `icon`: string or null, required. The single emoji shown beside the note's name, or `null`.
  - `cover`: string or null, required. The note's cover image, as a URL this key's user can load with their session or key, or `null`. Not part of the note's content, and not one of its files.
  - `tags`: array of objects, required. The note's labels, by name. Tags belong to the user, not to one note.
    - `id`: string (uuid), required
    - `name`: string, required
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last content update timestamp.
  - `fullWidth`: boolean, required
  - `contentFormat`: `"tiptap"`, required. Current serialization of `content`.
  - `contentUnstable`: `true`, required. Editor-specific TipTap JSON. Node and attribute shapes may change.
  - `content`: any value, required. Editor document, returned verbatim. See `contentUnstable`.
  - `markdown`: string or null, required. The note as markdown. Writing it back with `PUT /notes/{noteId}/content` reproduces the note exactly when `markdownLosses` is empty. Otherwise the listed parts come back simplified. Block ids are not in the markdown; a write mints new ones. An empty image alt or title reads back as null. Null when the note's content isn't an editor document.
  - `markdownLosses`: array of one of `"empty-paragraphs"`, `"collapsed-spaces"`, `"image-size"`, `"table-layout"`, `"table-cell-blocks"`, `"table-header"`, `"table-cell-background"`, `"ordered-list-type"`, `"image-upload-state"`, `"inline-math"`, `"mention-label"`, `"formatting-edges"`, `"highlight"`, `"text-colour"`, `"toggle-summary-formatting"`, `"toggle-heading-level"`, `"code-block-language"`, `"code-block-filename"`, `"code-block-wrap"`, `"block-math"`, `"image-address"`, `"image-loading"`, `"link-address"`, `"link-title"`, `"link-attributes"`, `"mention-target"`, `"date-chip"`, `"heading-level"`, `"ordered-list-start"`, `"callout-kind"`, `"callout-nesting"`, `"columns"`, `"table-of-contents"`, `"footnote-orphan"`, `"youtube-embed"`, `"pdf-embed"`, `"recording-embed"`, `"bookmark-card"`, `"citation-text"`, `"bibliography"`, `"invalid-structure"`, `"unsupported-mark"`, `"unsupported-content"`, `"exceeds-write-limit"`, required. What `markdown` simplifies, each value once. Empty when it reproduces the note exactly. - `empty-paragraphs`: Empty paragraphs are left out. One empty paragraph at the end of the note is not reported, because the editor adds it back. - `collapsed-spaces`: Runs of spaces, tabs and line breaks inside text become one space. Spaces at the start or end of a paragraph, heading or cell, or next to a hard break, are dropped. A hard break at the end of a paragraph is dropped, and one in a heading becomes a space. - `image-size`: Image width and height are left out. - `table-layout`: Merged cells, column widths and cell alignment are left out. - `table-cell-blocks`: A cell holding a list, a quote, a callout, a toggle, columns, code, a formula or more than one paragraph is written as one line: each paragraph, heading or toggle summary in it on a line of its own, separated by `<br>`, with a formula as inline math and code as its text. List bullets and checkboxes, quote and callout markers, images and embeds in the cell are left out, and reading the Markdown back gives one paragraph with line breaks. Footnote markers, math, citations, dates and mentions are written as anywhere else. - `table-header`: The first row is written as the header even when its cells aren't header cells, and header cells in later rows become normal cells. - `table-cell-background`: Cell background colours are left out. - `ordered-list-type`: Letter and Roman numbering become plain numbers. - `image-upload-state`: An image that never finished uploading is written as its alt text, and its upload details are left out. - `inline-math`: Inline math that `$…$` can't hold is written as text: empty math, latex with an unescaped `$` or a trailing backslash, and math followed directly by a digit. Spaces at either end of the latex are trimmed. - `mention-label`: A mention without a label gets its kind as the label, and line breaks in a label become spaces. Label-only export turns mentions into plain text. - `formatting-edges`: Bold, italic and strikethrough can't start or end on a space, so the space moves outside the formatting. Where they can't start or end at all, such as bold text that starts with punctuation right after a letter, the paragraph's bold, italic and strikethrough are dropped. - `highlight`: Highlighting is dropped while its text is kept. - `text-colour`: Text colour is dropped while the text is kept. - `toggle-summary-formatting`: A toggle's summary is written as plain text, so bold, italic, underline, strikethrough, links and inline code in it keep their text and lose their formatting. Whether the toggle was open is never stored, so there is nothing to report for it. - `toggle-heading-level`: A toggle whose summary reads as a heading keeps the heading's text and loses its level. The summary is written inside a `<summary>` tag, which carries the text and nothing else. - `code-block-language`: A code block language that cannot fit in a Markdown fence is left out. - `code-block-filename`: A code block's filename is left out. A Markdown fence carries the language and nothing else. - `code-block-wrap`: A code block's soft-wrap setting is left out. It is a view preference, so the code itself is unchanged. - `block-math`: A formula line containing only $$ is changed to $ $. - `image-address`: An image address is normalized, or the image becomes alt text when its address is refused. - `image-loading`: The image loading preference is reset to lazy. - `link-address`: A link address is normalized, or the link becomes text when its address is refused. - `link-title`: A numeric or boolean link title is read back as that value instead of text. - `link-attributes`: The link target, rel or CSS class is reset to the editor default. - `mention-target`: A mention with an invalid kind or ID becomes plain text. - `date-chip`: An inline date is written as its day in `YYYY-MM-DD` form, as plain text. Reading the Markdown back gives that text rather than the chip. - `heading-level`: An unsupported heading level becomes level 1 or 4. - `ordered-list-start`: A list start that Markdown cannot represent becomes 1. - `callout-kind`: A callout of an unknown kind is written as a note. - `callout-nesting`: A callout directly inside another callout is written as part of the callout around it, and loses its own kind. A callout further down, inside a quote inside a callout, is written as a deeper quote and keeps its kind. - `columns`: Markdown has no columns, so a columns block is written as its columns one after another, left to right, and the widths are left out. Each block in them is written as it would be on its own, in a quote or a toggle too, and reading the Markdown back gives those blocks in one column. A columns block in a table cell is written as part of the cell's one line instead, as `table-cell-blocks` describes. - `table-of-contents`: A table of contents block is written as a `[TOC]` line. Its rows are the note's own headings, which the Markdown already holds, so nothing in them is lost; reading the Markdown back gives a paragraph of that text rather than the block. - `footnote-orphan`: A footnote marker with no footnote, and a footnote no marker points at, are both left out. The editor pairs them up, so this only happens to a note written through the API. - `youtube-embed`: A YouTube block is written as a link to the video. Reading the Markdown back gives the link, not the player. - `pdf-embed`: A PDF block is written as a link to the file. Reading the Markdown back gives the link, not the viewer. - `recording-embed`: A recording's audio player is written as its label. The recording itself is not part of the Markdown, so reading it back gives a line of text. - `bookmark-card`: A bookmark card is written as a link, `[title](address)`, using the page title the card shows. Its description and site line are left out, and reading the Markdown back gives the link rather than the card. - `citation-text`: A citation is written as the bracketed text its style reads as, such as `(Smith, 2024, p. 3)`, as plain text. The source behind it is not written, so reading the Markdown back gives that text rather than the citation. - `bibliography`: A bibliography is written as a heading naming the list as its style does (`References` in APA, `Works Cited` in MLA, `Reference list` in Harvard) over a plain bullet list of its entries, each one the reference its style reads as, in the order the note lists them. The sources behind them are not written, so reading the Markdown back gives a heading and a list rather than the note's bibliography. - `invalid-structure`: Malformed table, list or code block structure is simplified. - `unsupported-mark`: An unknown mark is dropped while its text is kept. - `unsupported-content`: An unknown node is reduced to its text. - `exceeds-write-limit`: The markdown is longer than `PUT /notes/{noteId}/content` accepts (100,000 characters). Append with `POST /notes/{noteId}/content/append`, or edit the note in the app.
  - `version`: integer, required. Increases on every content change. Send it as `baseVersion` to avoid overwriting a newer save.
  - `pinned`: boolean, required. Pinned on the notes home.
  - `summary`: object or null, required
    - `summary`: string, required
    - `keywords`: array of string, required
    - `createdAt`: string (date-time), required. When the summary was generated.

#### Example

```json
{
  "data": {
    "id": "3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a",
    "name": "Photosynthesis",
    "folder": {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null
    },
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "icon": "🌱",
    "cover": "https://workroad.example/api/storage/note/3c2b1a09-8f7e-4d6c-9b5a-4f3e2d1c0b9a/cover-1a2b3c4d.jpg",
    "tags": [
      {
        "id": "5f4e3d2c-1b0a-4938-8776-655443322110",
        "name": "Exam"
      }
    ],
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "fullWidth": false,
    "contentFormat": "tiptap",
    "contentUnstable": true,
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a"
          },
          "content": [
            {
              "type": "text",
              "text": "Light becomes chemical energy."
            }
          ]
        }
      ]
    },
    "markdown": "Light becomes chemical energy.",
    "markdownLosses": [],
    "version": 3,
    "pinned": false,
    "summary": {
      "summary": "How plants turn light into sugar.",
      "keywords": [
        "chlorophyll",
        "glucose",
        "light reactions"
      ],
      "createdAt": "2026-02-01T12:00: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 is in the trash for this key's user.
- `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 note folders

`GET https://workroad.app/api/v1/note-folders`

Every folder owned by the key's user, sorted by name, then id.

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

### Response

`200` OK

- `data`: array of objects, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `subjectId`: string or null, required. Subject id, when set.
  - `icon`: one of `"Folder"`, `"File"`, `"Book"`, `"Bookmark"`, `"Star"`, `"Heart"`, `"Flag"`, `"Tag"`, `"Inbox"`, `"Archive"`, `"GraduationCap"`, `"School"`, `"Backpack"`, `"Notebook"`, `"Pencil"`, `"Ruler"`, `"Calculator"`, `"Globe"`, `"Microscope"`, `"TestTube"`, `"Dna"`, `"Atom"`, `"Rocket"`, `"Telescope"`, `"Code"`, `"Terminal"`, `"Database"`, `"Server"`, `"Cpu"`, `"Wifi"`, `"Cloud"`, `"Bug"`, `"QrCode"`, `"PlusSquare"`, `"MinusSquare"`, `"DivideSquare"`, `"Percent"`, `"Sigma"`, `"Pi"`, `"Infinity"`, `"BarChart"`, `"LineChart"`, `"Magnet"`, `"Thermometer"`, `"Leaf"`, `"Droplet"`, `"Flame"`, `"Zap"`, `"Wind"`, `"Palette"`, `"Music"`, `"Camera"`, `"Video"`, `"Brush"`, `"Pen"`, `"Scissors"`, `"Shapes"`, `"Briefcase"`, `"TrendingUp"`, `"PieChart"`, `"DollarSign"`, `"CreditCard"`, `"ShoppingCart"`, `"Activity"`, `"Stethoscope"`, `"Apple"`, `"Dumbbell"`, `"Calendar"`, `"Clock"`, `"Timer"`, `"Hourglass"`, `"AlarmClock"`, `"MessageSquare"`, `"Mail"`, `"Phone"`, `"Share"`, `"Link"`, `"Coffee"`, `"Lightbulb"`, `"Map"`, `"Compass"`, `"Target"`, `"Trophy"`, `"Puzzle"`, `"Gift"`, `"Smile"`, required. Icon name shown in the UI.

#### Example

```json
{
  "data": [
    {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Biology",
      "subjectId": null,
      "icon": "Folder"
    }
  ]
}
```

### 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 endpoint exists at this path.
- `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

## Create a note folder

`POST https://workroad.app/api/v1/note-folders`

Creates a folder, optionally filed under one of the user's subjects by `subjectId` or `subjectName`.

- MCP tool: `createNoteFolder` on `https://workroad.app/api/mcp`
- Required scopes: notes:create
- Test mode: `POST https://workroad.app/api/test/v1/note-folders` checks the request and returns the example response without changing anything.

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

- `name`: string, required, 1-64 characters
- `subjectId`: string (uuid). Subject id owned by the user.
- `subjectName`: string, 1-64 characters. The subject (the course or module, such as `Software Engineering`) by name instead of by id. It must match one of the user's active subjects, ignoring case and accents. A name that matches none is refused with 400 `unknown-subject`, whose `details.subjects` lists the user's subjects, unless `createSubjectIfMissing` is true. Send `subjectId` or `subjectName`, not both.
- `createSubjectIfMissing`: boolean. With `subjectName`, creates the subject when the user has none by that name. Needs the `subjects:create` scope. Only send it when the user named a course they haven't added yet.
- `icon`: one of `"Folder"`, `"File"`, `"Book"`, `"Bookmark"`, `"Star"`, `"Heart"`, `"Flag"`, `"Tag"`, `"Inbox"`, `"Archive"`, `"GraduationCap"`, `"School"`, `"Backpack"`, `"Notebook"`, `"Pencil"`, `"Ruler"`, `"Calculator"`, `"Globe"`, `"Microscope"`, `"TestTube"`, `"Dna"`, `"Atom"`, `"Rocket"`, `"Telescope"`, `"Code"`, `"Terminal"`, `"Database"`, `"Server"`, `"Cpu"`, `"Wifi"`, `"Cloud"`, `"Bug"`, `"QrCode"`, `"PlusSquare"`, `"MinusSquare"`, `"DivideSquare"`, `"Percent"`, `"Sigma"`, `"Pi"`, `"Infinity"`, `"BarChart"`, `"LineChart"`, `"Magnet"`, `"Thermometer"`, `"Leaf"`, `"Droplet"`, `"Flame"`, `"Zap"`, `"Wind"`, `"Palette"`, `"Music"`, `"Camera"`, `"Video"`, `"Brush"`, `"Pen"`, `"Scissors"`, `"Shapes"`, `"Briefcase"`, `"TrendingUp"`, `"PieChart"`, `"DollarSign"`, `"CreditCard"`, `"ShoppingCart"`, `"Activity"`, `"Stethoscope"`, `"Apple"`, `"Dumbbell"`, `"Calendar"`, `"Clock"`, `"Timer"`, `"Hourglass"`, `"AlarmClock"`, `"MessageSquare"`, `"Mail"`, `"Phone"`, `"Share"`, `"Link"`, `"Coffee"`, `"Lightbulb"`, `"Map"`, `"Compass"`, `"Target"`, `"Trophy"`, `"Puzzle"`, `"Gift"`, `"Smile"`, default `"Folder"`. Icon name shown in the UI.

### Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `subjectId`: string or null, required. Subject id, when set.
  - `icon`: one of `"Folder"`, `"File"`, `"Book"`, `"Bookmark"`, `"Star"`, `"Heart"`, `"Flag"`, `"Tag"`, `"Inbox"`, `"Archive"`, `"GraduationCap"`, `"School"`, `"Backpack"`, `"Notebook"`, `"Pencil"`, `"Ruler"`, `"Calculator"`, `"Globe"`, `"Microscope"`, `"TestTube"`, `"Dna"`, `"Atom"`, `"Rocket"`, `"Telescope"`, `"Code"`, `"Terminal"`, `"Database"`, `"Server"`, `"Cpu"`, `"Wifi"`, `"Cloud"`, `"Bug"`, `"QrCode"`, `"PlusSquare"`, `"MinusSquare"`, `"DivideSquare"`, `"Percent"`, `"Sigma"`, `"Pi"`, `"Infinity"`, `"BarChart"`, `"LineChart"`, `"Magnet"`, `"Thermometer"`, `"Leaf"`, `"Droplet"`, `"Flame"`, `"Zap"`, `"Wind"`, `"Palette"`, `"Music"`, `"Camera"`, `"Video"`, `"Brush"`, `"Pen"`, `"Scissors"`, `"Shapes"`, `"Briefcase"`, `"TrendingUp"`, `"PieChart"`, `"DollarSign"`, `"CreditCard"`, `"ShoppingCart"`, `"Activity"`, `"Stethoscope"`, `"Apple"`, `"Dumbbell"`, `"Calendar"`, `"Clock"`, `"Timer"`, `"Hourglass"`, `"AlarmClock"`, `"MessageSquare"`, `"Mail"`, `"Phone"`, `"Share"`, `"Link"`, `"Coffee"`, `"Lightbulb"`, `"Map"`, `"Compass"`, `"Target"`, `"Trophy"`, `"Puzzle"`, `"Gift"`, `"Smile"`, required. Icon name shown in the UI.

#### Example

```json
{
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "name": "Biology",
    "subjectId": null,
    "icon": "Folder"
  }
}
```

### Errors

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

- `400`: Invalid body, or a `subjectId` that is not one of the user's subjects (`details.reason` is `unknown-subject`).
- `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 endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: Another folder of the user's already has that name (`details.reason` is `name-taken`). 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

## Update a note folder

`PATCH https://workroad.app/api/v1/note-folders/{folderId}`

Changes only the fields you send; `null` on `subjectId` unfiles the folder. Renaming to a name another of the user's folders already has is refused, as when creating one.

- MCP tool: `updateNoteFolder` on `https://workroad.app/api/mcp`
- Required scopes: notes:update
- Test mode: `PATCH https://workroad.app/api/test/v1/note-folders/{folderId}` checks the request and returns the example response without changing anything.

### Path parameters

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

### Request body

JSON.

- `name`: string, 1-64 characters
- `subjectId`: string (uuid) or null. Subject id owned by the user. `null` unfiles the folder.
- `icon`: one of `"Folder"`, `"File"`, `"Book"`, `"Bookmark"`, `"Star"`, `"Heart"`, `"Flag"`, `"Tag"`, `"Inbox"`, `"Archive"`, `"GraduationCap"`, `"School"`, `"Backpack"`, `"Notebook"`, `"Pencil"`, `"Ruler"`, `"Calculator"`, `"Globe"`, `"Microscope"`, `"TestTube"`, `"Dna"`, `"Atom"`, `"Rocket"`, `"Telescope"`, `"Code"`, `"Terminal"`, `"Database"`, `"Server"`, `"Cpu"`, `"Wifi"`, `"Cloud"`, `"Bug"`, `"QrCode"`, `"PlusSquare"`, `"MinusSquare"`, `"DivideSquare"`, `"Percent"`, `"Sigma"`, `"Pi"`, `"Infinity"`, `"BarChart"`, `"LineChart"`, `"Magnet"`, `"Thermometer"`, `"Leaf"`, `"Droplet"`, `"Flame"`, `"Zap"`, `"Wind"`, `"Palette"`, `"Music"`, `"Camera"`, `"Video"`, `"Brush"`, `"Pen"`, `"Scissors"`, `"Shapes"`, `"Briefcase"`, `"TrendingUp"`, `"PieChart"`, `"DollarSign"`, `"CreditCard"`, `"ShoppingCart"`, `"Activity"`, `"Stethoscope"`, `"Apple"`, `"Dumbbell"`, `"Calendar"`, `"Clock"`, `"Timer"`, `"Hourglass"`, `"AlarmClock"`, `"MessageSquare"`, `"Mail"`, `"Phone"`, `"Share"`, `"Link"`, `"Coffee"`, `"Lightbulb"`, `"Map"`, `"Compass"`, `"Target"`, `"Trophy"`, `"Puzzle"`, `"Gift"`, `"Smile"`. Icon name shown in the UI.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `subjectId`: string or null, required. Subject id, when set.
  - `icon`: one of `"Folder"`, `"File"`, `"Book"`, `"Bookmark"`, `"Star"`, `"Heart"`, `"Flag"`, `"Tag"`, `"Inbox"`, `"Archive"`, `"GraduationCap"`, `"School"`, `"Backpack"`, `"Notebook"`, `"Pencil"`, `"Ruler"`, `"Calculator"`, `"Globe"`, `"Microscope"`, `"TestTube"`, `"Dna"`, `"Atom"`, `"Rocket"`, `"Telescope"`, `"Code"`, `"Terminal"`, `"Database"`, `"Server"`, `"Cpu"`, `"Wifi"`, `"Cloud"`, `"Bug"`, `"QrCode"`, `"PlusSquare"`, `"MinusSquare"`, `"DivideSquare"`, `"Percent"`, `"Sigma"`, `"Pi"`, `"Infinity"`, `"BarChart"`, `"LineChart"`, `"Magnet"`, `"Thermometer"`, `"Leaf"`, `"Droplet"`, `"Flame"`, `"Zap"`, `"Wind"`, `"Palette"`, `"Music"`, `"Camera"`, `"Video"`, `"Brush"`, `"Pen"`, `"Scissors"`, `"Shapes"`, `"Briefcase"`, `"TrendingUp"`, `"PieChart"`, `"DollarSign"`, `"CreditCard"`, `"ShoppingCart"`, `"Activity"`, `"Stethoscope"`, `"Apple"`, `"Dumbbell"`, `"Calendar"`, `"Clock"`, `"Timer"`, `"Hourglass"`, `"AlarmClock"`, `"MessageSquare"`, `"Mail"`, `"Phone"`, `"Share"`, `"Link"`, `"Coffee"`, `"Lightbulb"`, `"Map"`, `"Compass"`, `"Target"`, `"Trophy"`, `"Puzzle"`, `"Gift"`, `"Smile"`, required. Icon name shown in the UI.

#### Example

```json
{
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "name": "Biology",
    "subjectId": null,
    "icon": "Folder"
  }
}
```

### Errors

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

- `400`: Invalid body, or a `subjectId` that is not one of the user's subjects (`details.reason` is `unknown-subject`).
- `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 folder with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: Another folder of the user's already has that name (`details.reason` is `name-taken`).
- `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 note folder to the trash

`DELETE https://workroad.app/api/v1/note-folders/{folderId}`

Moves the folder and every note in it to the trash. `POST /note-folders/{folderId}/restore` brings back the folder and the notes that went with it. The trash is emptied after 30 days.

- MCP tool: `deleteNoteFolder` on `https://workroad.app/api/mcp`
- Required scopes: notes:delete
- Test mode: `DELETE https://workroad.app/api/test/v1/note-folders/{folderId}` checks the request and returns the example response without changing anything.
- Next: `restoreNoteFolder` brings it back from the trash.

### Path parameters

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

### Response

`200` OK

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

#### Example

```json
{
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "deleted": true
  }
}
```

### 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 folder 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

## Restore a note folder from the trash

`POST https://workroad.app/api/v1/note-folders/{folderId}/restore`

Brings a trashed folder back with the notes that went to the trash with it, and returns the folder. Needs `notes:delete`, the scope that trashed it. If a live folder took its name meanwhile, the restored folder is renamed `name (2)`, `name (3)` and so on. Notes trashed on their own before the folder stay in the trash.

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

### Path parameters

- `folderId`: string (uuid), required. Note folder 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
  - `name`: string, required
  - `subjectId`: string or null, required. Subject id, when set.
  - `icon`: one of `"Folder"`, `"File"`, `"Book"`, `"Bookmark"`, `"Star"`, `"Heart"`, `"Flag"`, `"Tag"`, `"Inbox"`, `"Archive"`, `"GraduationCap"`, `"School"`, `"Backpack"`, `"Notebook"`, `"Pencil"`, `"Ruler"`, `"Calculator"`, `"Globe"`, `"Microscope"`, `"TestTube"`, `"Dna"`, `"Atom"`, `"Rocket"`, `"Telescope"`, `"Code"`, `"Terminal"`, `"Database"`, `"Server"`, `"Cpu"`, `"Wifi"`, `"Cloud"`, `"Bug"`, `"QrCode"`, `"PlusSquare"`, `"MinusSquare"`, `"DivideSquare"`, `"Percent"`, `"Sigma"`, `"Pi"`, `"Infinity"`, `"BarChart"`, `"LineChart"`, `"Magnet"`, `"Thermometer"`, `"Leaf"`, `"Droplet"`, `"Flame"`, `"Zap"`, `"Wind"`, `"Palette"`, `"Music"`, `"Camera"`, `"Video"`, `"Brush"`, `"Pen"`, `"Scissors"`, `"Shapes"`, `"Briefcase"`, `"TrendingUp"`, `"PieChart"`, `"DollarSign"`, `"CreditCard"`, `"ShoppingCart"`, `"Activity"`, `"Stethoscope"`, `"Apple"`, `"Dumbbell"`, `"Calendar"`, `"Clock"`, `"Timer"`, `"Hourglass"`, `"AlarmClock"`, `"MessageSquare"`, `"Mail"`, `"Phone"`, `"Share"`, `"Link"`, `"Coffee"`, `"Lightbulb"`, `"Map"`, `"Compass"`, `"Target"`, `"Trophy"`, `"Puzzle"`, `"Gift"`, `"Smile"`, required. Icon name shown in the UI.

#### Example

```json
{
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "name": "Biology",
    "subjectId": null,
    "icon": "Folder"
  }
}
```

### 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 folder with that id is in the trash for this key's user.
- `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
