# Notes

A note is one entry in a thread: long text, lists, checklists, tables, code, links, images and files.

## The note object

`GET /notes/{note_id}` answers a note in full:

- `text`, `markdown` and `document`: the whole content in three forms. `document` is the rich document the editor saves.
- `revision` changes with any change; `content_revision` only when the text changes.
- `context_receipt` and `editable`: what you send to change the note, and whether this connection may change it now. `context_receipt` is `null` when it may not.
- `origin` and `last_change_origin`: who added the note and who last changed its text.
- `added_via` and `changed_via`: the connection that added it, and the one that made the latest content change.
- `images` and `files`: previews and descriptions of what the document holds. Originals are not available through connections.
- `pinned`, `done`, `trashed`, `thread`, `url`, `created_at` and `updated_at`.

Lists and search answer shorter items with a `text_preview`. `truncated: true` means the preview is cut: read the note before you quote it.

## Add a note

Add a note at the end of a thread. Send the content in one of three forms:

```json
{"content": {"text": "Build 412 passed.\n\nDeployed to staging."}}
```

```json
{"content": {"markdown": "## Release 412\n\n- [x] Tests passed\n- [ ] Tag the build\n\n| Check | Result |\n| --- | --- |\n| Smoke | OK |"}}
```

```json
{
  "content": {
    "document": {
      "format": "work-threads.richtext",
      "schemaVersion": 4,
      "doc": {
        "type": "doc",
        "content": [
          {"type": "heading", "attrs": {"level": 2}, "content": [{"type": "text", "text": "Release 412"}]},
          {"type": "bulletList", "content": [
            {"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Tests passed", "marks": [{"type": "bold"}]}]}]},
            {"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Deployed to staging"}]}]}
          ]}
        ]
      }
    }
  }
}
```

A document keeps everything the editor can hold: headings go up to level 3, a list item starts with a paragraph, and every table row has the same number of cells. The [formatting guide](https://thread.page/agents/threadpage/references/formatting.md) lists every element.

### Add a note to a thread

`POST /api/integrations/v1/threads/{thread_id}/notes`

Idempotent: repeat the same Operation-Id header and body to get the same receipt; a different body with a used Operation-Id is rejected with 409.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Add only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `create_note` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `thread_id` | path | uuid, required |

**Body (application/json)**

| Field | Type |
|---|---|
| `content` | text, Markdown or a document, required |

Answer `201`: Accepted, or the same receipt again.

**Answer fields**

| Field | Type |
|---|---|
| `operation_id` | uuid, required |
| `status` | `accepted`, `conflict`, `not_completed`, required |
| `note_id` | uuid |
| `revision` | integer |
| `accepted_at` | date-time |
| `url` | uri |
| `code` | string |

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/threads/$THREAD_ID/notes" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -H "Content-Type: application/json" \
  -d '{"content":{"text":"Build 412 passed.\n\nDeployed to staging."}}'
```

Errors: `write_not_allowed` 403, 409, `unavailable` 404, `invalid_json` 400, `invalid_content` 400, `invalid_document` 400, `content_too_large` 413, `unsupported_content` 413, 422, `unsupported_media_type` 415, `operation_id_reused` 409, `shared_place_write` 409, `thread_archived` 409, `entity_deleted` 409, `image_unavailable` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Add a note to the Inbox

`POST /inbox/notes` takes the same body and works on every plan. It is the one write a connection has on Free.

- With Pro and Sync, the note is in the Inbox thread at once: the receipt says `"delivery": "account"` and has its `url`.
- Otherwise it waits for the browser that collects the owner’s Inbox, for up to 30 days: the receipt says `"delivery": "inbox"` and `url` is `null`. That browser keeps the `note_id`.
- On Free, connections add up to 3,000 notes a month (UTC), all of them together. Then `429` `quota_exhausted` answers without `Retry-After`, and `details.resets_at` says when the count starts again.
- Up to 1,000 notes or 20 MB can wait at once. Past that, `409` `inbox_full`.
- A note with an image needs Pro and a level above Add to Inbox.

### Add a note to the owner's Inbox

`POST /api/integrations/v1/inbox/notes`

Every plan, and the one write on Free. With Pro and Sync the note goes into the owner's cloud Inbox at once (delivery account); otherwise it waits up to 30 days for the browser that collects the owner's Inbox, which keeps note_id (delivery inbox). Idempotent: repeat the same Operation-Id header and body to get the same receipt, even after the month's notes are used or Pro ended; a different body is rejected with 409. On Free, connections add up to capabilities.free.connection_notes notes a month (UTC): then 429 quota_exhausted without Retry-After, details.resets_at saying when. Images need Pro.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Add only · Add to Inbox |
| Plan | Every plan. |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `create_note` |

**Body (application/json)**

| Field | Type |
|---|---|
| `content` | text, Markdown or a document, required |

Answer `201`: Accepted, or the same receipt.

**Answer fields**

| Field | Type | Description |
|---|---|---|
| `operation_id` | uuid, required |  |
| `status` | `accepted`, required |  |
| `note_id` | uuid, required | The note's id: the browser that collects it keeps this id. |
| `delivery` | `inbox`, `account`, required | inbox: it waits for the browser that collects the owner's Inbox; account: it is in the cloud Inbox already (Pro with Sync). |
| `accepted_at` | date-time, required |  |
| `url` | uri or null, required | The note in ThreadPage; null while it waits. |

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/inbox/notes" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -H "Content-Type: application/json" \
  -d '{"content":{"text":"Build 412 passed."}}'
```

Errors: `write_not_allowed` 403, 409, `invalid_json` 400, `invalid_content` 400, `invalid_document` 400, `content_too_large` 413, `unsupported_media_type` 415, `operation_id_reused` 409, `image_unavailable` 409, `quota_exhausted` 429, `inbox_full` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get).

## Read a note

Read one note in full, or list a thread’s notes, newest first, as previews.

### Get a note with its full content

`GET /api/integrations/v1/notes/{note_id}`

Plain text, Markdown and the rich document (RichDocument v4), with content_revision and context_receipt for a later edit.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `get_note` |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |

Answer `200`.

**Answer fields**

| Field | Type | Description |
|---|---|---|
| `id` | uuid, required |  |
| `thread_id` | uuid, required |  |
| `thread` | object, required |  |
| `revision` | integer, required |  |
| `content_revision` | integer, required | Moves with every change to the text; compare it to tell newer text from older. |
| `created_at` | date-time, required |  |
| `updated_at` | date-time, required |  |
| `pinned` | boolean, required |  |
| `done` | boolean, required |  |
| `trashed` | boolean, required |  |
| `origin` | `human`, `service`, `agent`, `participant`, required |  |
| `last_change_origin` | `human`, `service`, `agent`, `participant`, required | Who made the latest change to the text, in the same terms as origin. |
| `added_via` | object or  or null or null, required |  |
| `changed_via` | object or  or null or null, required | The connection whose change is the note's latest content change; null when a person changed it last or nobody changed it since it was added. |
| `editable` | boolean, required | Whether this connection may change this note now: its level, Read and add's own notes, shared places and request notes. |
| `text` | string, required | Plain text of the whole note. |
| `markdown` | string, required | The whole note as Markdown, as events and requests carry it; an image or a note reference becomes a link to the note. |
| `document` | object, required | RichDocument v4 (format work-threads.richtext). |
| `images` | array of object, required | The images the document holds, in its order; an image whose bytes have not reached the server yet is left out. |
| `files` | array of object, required | The other files the document holds, in its order (getNoteFile): those of its file blocks, and a file an image block holds that is no image (such as a TIFF); one whose bytes have not reached the server yet, or that was deleted, is left out. |
| `url` | uri, required |  |
| `context_receipt` | string or null, required | Send it with PUT /notes/{note_id}/content or a version restore; null when this connection may not change the note. |

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

### List notes in a thread, newest first

`GET /api/integrations/v1/threads/{thread_id}/notes`

Each item carries a text preview; read a note for its full content.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `get_thread` |

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `cursor` | query | string | From next_cursor. |
| `limit` | query | integer | 1–50, default 20. |
| `thread_id` | path | uuid, required |  |

Answer `200`.

**Answer fields**

| Field | Type |
|---|---|
| `items` | array of object, required |
| `next_cursor` | string or null, required |

```bash
curl -m 30 "https://thread.page/api/integrations/v1/threads/$THREAD_ID/notes" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404, `context_changed` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Search notes

`GET /search?q=` finds notes whose text, or whose thread’s title or description, contains a phrase of 1 to 200 characters, ignoring case. It answers notes, not threads.

- `thread_id` searches one thread. `folder_id` searches the threads directly in that folder, not those in its subfolders.
- `state` is `active` (the default), `trash` or `all`.
- `limit` takes 1 to 50 and defaults to 20.

### Search notes by text or thread title or description

`GET /api/integrations/v1/search`

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `search` |

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `cursor` | query | string | From next_cursor. |
| `folder_id` | query | uuid | Only notes of threads directly in this folder. |
| `limit` | query | integer | 1–50, default 20. |
| `q` | query | string, required | 1–200 characters. |
| `state` | query | `active`, `all`, `trash` | Default active. |
| `thread_id` | query | uuid |  |

Answer `200`. Its fields are those under [List notes in a thread, newest first](#listThreadNotes).

```bash
curl -m 30 "https://thread.page/api/integrations/v1/search?q=release" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `context_changed` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Edit a note safely

To change a note, read it, change its document, and send the result with the `content_revision` and `context_receipt` of that read. ThreadPage merges your change with edits made since the revision you read; only an overlap is refused. If it is refused, read the note again and apply your change again.

JavaScript:

```javascript
// Edit a note without losing edits made since you read it.
// Run with Node.js 24: THREADPAGE_TOKEN=tp_… NOTE_ID=… node edit_note.mjs
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';

const API = 'https://thread.page/api/integrations/v1';
const AUTH = { Authorization: `Bearer ${process.env.THREADPAGE_TOKEN}` };

// PUT /notes/{note_id}/content. After a lost answer, or a retryable 429 or 503, it sends the
// identical request again with the same Operation-Id.
async function putContent(noteId, body, operationId) {
  for (let attempt = 1; ; attempt++) {
    let answer;
    try {
      answer = await fetch(`${API}/notes/${noteId}/content`, {
        method: 'PUT',
        headers: { ...AUTH, 'Operation-Id': operationId, 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(30_000),
      });
    } catch (error) {
      if (attempt === 3) throw error;
      await sleep(attempt * 2_000);
      continue;
    }
    const wait = Number(answer.headers.get('Retry-After'));
    if ([429, 503].includes(answer.status) && wait && wait <= 60 && attempt < 3) {
      await sleep(wait * 1_000);
      continue;
    }
    return answer;
  }
}

// Apply change(document) -> document without losing edits made since you read the note.
async function editNote(noteId, change, attempts = 3) {
  for (let attempt = 0; attempt < attempts; attempt++) {
    const read = await fetch(`${API}/notes/${noteId}`, { headers: AUTH });
    if (!read.ok) throw new Error(`${read.status}: ${await read.text()}`);
    const current = await read.json();
    if (!current.editable) throw new Error('This connection may not change this note; add a new note instead.');
    const body = {
      content: { document: change(current.document) },
      content_revision: current.content_revision,
      context_receipt: current.context_receipt,
    };
    const answer = await putContent(noteId, body, randomUUID()); // one ID for this write
    if (answer.status === 409) {
      const error = await answer.json();
      if (['stale_block', 'reread_required'].includes(error.code)) continue; // nothing was written: read again
      throw new Error(`409 ${error.code}: ${error.message}`);
    }
    if (!answer.ok) throw new Error(`${answer.status}: ${await answer.text()}`);
    return answer.json(); // the receipt
  }
  throw new Error('The note kept changing; ask the person to check it.');
}

// The change this example makes: one paragraph at the end of the note.
const addLine = document => ({
  ...document,
  doc: {
    ...document.doc,
    content: [...document.doc.content, { type: 'paragraph', content: [{ type: 'text', text: 'Checked by the release script.' }] }],
  },
});

const receipt = await editNote(process.env.NOTE_ID, addLine);
console.log(receipt.status, receipt.content_revision, receipt.url);
```

Python:

```python
# Edit a note without losing edits made since you read it.
# Run with Python 3.13: pip install requests, then
# THREADPAGE_TOKEN=tp_… NOTE_ID=… python edit_note.py
import os
import time
import uuid

import requests

API = "https://thread.page/api/integrations/v1"
AUTH = {"Authorization": f"Bearer {os.environ['THREADPAGE_TOKEN']}"}

def put_content(note_id, body, operation_id):
    """PUT /notes/{note_id}/content. After a lost answer, or a retryable 429 or 503, it sends the
    identical request again with the same Operation-Id."""
    for attempt in range(1, 4):
        try:
            answer = requests.put(
                f"{API}/notes/{note_id}/content",
                headers={**AUTH, "Operation-Id": operation_id},
                json=body,
                timeout=30,
            )
        except (requests.ConnectionError, requests.Timeout):
            if attempt == 3:
                raise
            time.sleep(attempt * 2)
            continue
        wait = int(answer.headers.get("Retry-After", 0))
        if answer.status_code in (429, 503) and 0 < wait <= 60 and attempt < 3:
            time.sleep(wait)
            continue
        return answer

def edit_note(note_id, change, attempts=3):
    """Apply change(document) -> document without losing edits made since you read the note."""
    for _ in range(attempts):
        note = requests.get(f"{API}/notes/{note_id}", headers=AUTH, timeout=30)
        note.raise_for_status()
        current = note.json()
        if not current["editable"]:
            raise RuntimeError("This connection may not change this note; add a new note instead.")
        body = {
            "content": {"document": change(current["document"])},
            "content_revision": current["content_revision"],
            "context_receipt": current["context_receipt"],
        }
        answer = put_content(note_id, body, str(uuid.uuid4()))  # one ID for this write
        if answer.status_code == 409 and answer.json()["code"] in ("stale_block", "reread_required"):
            continue  # nothing was written: read again and apply the change again
        answer.raise_for_status()
        return answer.json()  # the receipt
    raise RuntimeError("The note kept changing; ask the person to check it.")

def add_line(document):
    """The change this example makes: one paragraph at the end of the note."""
    paragraph = {"type": "paragraph", "content": [{"type": "text", "text": "Checked by the release script."}]}
    return {**document, "doc": {**document["doc"], "content": [*document["doc"]["content"], paragraph]}}

if __name__ == "__main__":
    receipt = edit_note(os.environ["NOTE_ID"], add_line)
    print(receipt["status"], receipt["content_revision"], receipt["url"])
```

A new `Operation-Id` for each pass of the loop is right: after a `409` nothing was written, so the next pass is a new write. A lost answer is different: send the identical request with the same ID, as the examples do.

Who may change which note:

- **Edit and organize** changes any note in its scope.
- **Read and add** changes the notes it added, also after a person edited them, and no others. Changing another note answers `403` `edit_not_allowed`. When it changes a note, the note names the connection in `changed_via`, and ThreadPage keeps the last version a person wrote and the note’s first version until the note is deleted permanently.
- A note a recipient wrote for a request (a question, a result or an answer) never changes, on any level: `409` `request_note_locked`.
- Disconnecting a connection and connecting it again makes a new connection: the notes the old one added are not its own.

Markdown or text replaces the whole note. When the note holds formatting that form can’t keep, the answer is `400` `lossy_markdown` and nothing changes: send the document.

### Replace a note's content

`PUT /api/integrations/v1/notes/{note_id}/content`

The MCP tool update_note. Send the content_revision you read in full with GET /notes/{note_id} and the context_receipt from that read. Edits others made meanwhile to other parts are kept; an overlap answers 409 stale_block, an out-of-date read 409 reread_required. A Markdown or text replace that would drop formatting answers 400 lossy_markdown: send the document instead. Read and add changes the notes it added, also after a person edited them, and no others (403 edit_not_allowed); a note a recipient wrote for a request never changes (409 request_note_locked). Edit and organize changes any note. Scope, shared places (shared_place_write) and the open-request rule (request_open) apply to every write.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| Revision | `content_revision` and `context_receipt` from `GET /notes/{note_id}`, in the body. |
| MCP | `update_note` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |

**Body (application/json)**

| Field | Type | Description |
|---|---|---|
| `content` | text, Markdown or a document, required |  |
| `content_revision` | integer, required | The content_revision you read in full with GET /notes/{note_id}. |
| `context_receipt` | string, required | From that GET /notes/{note_id}. |
| `evidence` | array of object | Other notes this edit relies on, each with the content_revision read. |

Answer `200`: Accepted, or the same receipt again.

**Answer fields**

| Field | Type | Description |
|---|---|---|
| `operation_id` | uuid, required |  |
| `status` | `accepted`, `conflict`, `not_completed`, required |  |
| `kind` | `note.update`, `note.properties`, `note.move`, `note.trash`, `note.restore`, `note.restore_version`, `thread.create`, `thread.update`, `thread.archive`, `thread.unarchive`, `thread.trash`, `thread.restore`, `thread.move`, `folder.create`, `folder.rename`, `folder.move`, `folder.trash`, `folder.restore`, `reminder.set`, `reminder.remove`, required |  |
| `target` | object, required |  |
| `revision` | integer or null | The target's revision after the change (informational). |
| `content_revision` | integer or null | A note's content revision after a content change. |
| `accepted_at` | date-time, required |  |
| `url` | uri or null |  |
| `thread_status` | string | thread.update: the status the Thread has after the change, which is the replacement when the one you sent was deleted. |
| `reminder` | object or  or null or null | reminder.set: the reminder as set; reminder.remove: null. |

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X PUT "https://thread.page/api/integrations/v1/notes/$NOTE_ID/content" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -H "Content-Type: application/json" \
  -d '{
  "content": {
    "markdown": "## Release 412\n\n- [x] Tests passed\n- [x] Tagged v1.8.0"
  },
  "content_revision": 3,
  "context_receipt": ".eJyrVkrLz1eyUspNzMlRKs7PTVWyUsrJLFKqBQBKxwbR:1vQx2a:Hk3mQ0Zp8Wc5T1nL4yJr2aVbE9sDf6gUo7iXqRb1sM"
}'
```

Errors: `edit_not_allowed` 403, `unavailable` 404, `invalid_json` 400, `invalid_content` 400, `invalid_document` 400, `lossy_markdown` 400, `content_too_large` 413, `unsupported_media_type` 415, `stale_block` 409, `reread_required` 409, `operation_id_reused` 409, `request_note_locked` 409, `request_open` 409, `shared_place_write` 409, `thread_archived` 409, `entity_deleted` 409, `image_unavailable` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Add an image to a note

1. Upload the image to the thread: one PNG, JPEG or WebP file up to 5 MB, 20 megapixels and 300 frames, as `multipart/form-data` under any field name, with an `Operation-Id`.
2. Put the receipt’s `markdown` on its own line in a note’s Markdown in the same thread, outside lists, quotes, tables and headings, with a description between the brackets: `![Build summary](attachment:IMAGE_ID)`.

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors https://thread.page/api/integrations/v1/threads/$THREAD_ID/images \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -F file=@screenshot.png
```

The same bytes with the same `Operation-Id` get the same receipt; other bytes with a used ID answer `409` `operation_id_reused`. ThreadPage stores the file exactly as sent, metadata included. A note can use an image this connection uploaded to its thread, or one a note it can read already holds; any other answers `409` `image_unavailable`. Images need Pro.

In n8n, use HTTP Request with Body Content Type **Form-Data**, one parameter of type **n8n Binary File**, and an `Operation-Id` header. For a new UUID per image, put a **Crypto** node before it (Action **Generate**, Type **UUID**) and set the header to `{{ $json.data }}`. In Make or Zapier, create the UUID in a step before the request, so a retried run sends the same one.

### Upload an image for a new note in a thread

`POST /api/integrations/v1/threads/{thread_id}/images`

Send one PNG, JPEG or WebP file up to 5 MB as multipart/form-data, under any field name; other fields are ignored. The type comes from the bytes, which are stored exactly as sent, with any metadata in the file. Put the returned markdown on its own line in a note's Markdown, in this thread, with a description between the brackets. Idempotent by the Operation-Id header: the same bytes get the same receipt; other bytes with a used Operation-Id are rejected with 409. Each connection uploads up to 100 MB of new images a day.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Add only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `upload_note_image` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `thread_id` | path | uuid, required |

**Body (multipart/form-data)**

| Field | Type | Description |
|---|---|---|
| `file` | binary, required | One PNG, JPEG or WebP image up to 5 MB, under any field name; other fields are ignored. |

Answer `201`: Accepted, or the same receipt again.

**Answer fields**

| Field | Type | Description |
|---|---|---|
| `operation_id` | uuid, required |  |
| `status` | `accepted`, required |  |
| `image_id` | uuid, required |  |
| `thread_id` | uuid, required |  |
| `media_type` | `image/png`, `image/jpeg`, `image/webp`, required | Found in the bytes. |
| `width` | integer, required |  |
| `height` | integer, required |  |
| `markdown` | string, required | ![](attachment:<image_id>): put it on its own line in a note's Markdown in this thread, with a description between the brackets. |
| `accepted_at` | date-time, required |  |

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/threads/$THREAD_ID/images" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -F file=@screenshot.png
```

Errors: `write_not_allowed` 403, 409, `unavailable` 404, `content_too_large` 413, `operation_id_reused` 409, `shared_place_write` 409, `thread_archived` 409, `entity_deleted` 409, `invalid_attachment` 422, `attachment_quota_exceeded` 413, `attachments_unavailable` 503, `image_busy` 503.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Read an image preview

Each entry of a note’s `images` has a `url` you read with the same token. It answers a new PNG at most 1600 px on its longer side and 1 MiB, without the original’s metadata. A GIF, AVIF or HEIC image, or one larger than 20 MB or 20 megapixels, answers its ready WebP or JPEG preview, up to 2048 px. Each connection reads up to 20 previews a minute and 500 a day.

### Read a preview of an image a note holds

`GET /api/integrations/v1/notes/{note_id}/images/{image_id}`

A new PNG of a PNG, JPEG or WebP image, oriented, at most 1600 px on its longer side and 1 MiB, without the original's metadata (no location); any other image (GIF, AVIF, HEIC, or larger than 20 MB or 20 megapixels) answers its ready WebP or JPEG preview, up to 2048 px, as it is. Originals are not available through connections. The note must be readable and hold the image; it may be in Trash. Each connection reads up to 20 previews a minute and 500 a day.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `read_note_attachment` |

**Parameters**

| Name | In | Type |
|---|---|---|
| `image_id` | path | uuid, required |
| `note_id` | path | uuid, required |

Answer `200`: The preview.

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID/images/$IMAGE_ID" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404, `unsupported_content` 413, 422, `attachment_blocked` 403, `attachment_checksum_mismatch` 409, `attachment_unavailable` 503.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Add a file to a note

Upload a file of any type up to 2 GB to a thread in three steps. `POST /threads/THREAD_ID/files` with a new `Operation-Id` starts the upload and returns URLs for its first parts, and `POST /files/FILE_ID/parts` gives the next ones. `PUT` each part, then send every part’s ETag to `POST /files/FILE_ID/complete`. When `GET /operations/OPERATION_ID` says `uploaded`, add the file to a note in that thread with the `[name](attachment:FILE_ID)` line it returns, on a line of its own. The OpenAPI description lists every field.

| Request | Returns or does |
|---|---|
| `POST /threads/{id}/files` | Start uploading a file for a note in the thread |
| `POST /files/{id}/parts` | More part URLs for a file upload |
| `POST /files/{id}/complete` | Complete a file upload |

Each of them needs a token that can add notes. `GET /notes/NOTE_ID` lists the note’s files in `files`, each with its name, type and size; [Read a file](#read-a-file) gives their text and previews.

Each connection can upload 2 GB of files a day, and all connections of an account together 4 GB; over that the API answers `429` with `rate_limited` and `details.reason` `file_bytes_day`. Files also count toward your account’s [upload limits](https://thread.page/help/storage#upload-limits). A file sent through MCP can be up to 5 MB in one request. It counts toward the same daily limit as images: 100 MB for each connection and 250 MB for your account.

`409` with `image_unavailable` means a note names a file this connection didn’t upload to that thread, or hasn’t finished uploading. `422` with `invalid_attachment` means the file can’t be stored. `413` with `attachment_quota_exceeded` means the workspace’s storage is full. `410` with `attachment_erased` means the file was deleted and can’t be uploaded again. `503` with `attachment_upload_in_progress` means ThreadPage is still checking the file: send the note again after `Retry-After`. `403` with `attachment_blocked` means ThreadPage blocked the file; trying again won’t help.

## Read a file

`files` lists the other files a note holds. For each, read what it is, its ready preview and, for a text file or a PDF, its text.

- The preview is a WebP of an image (up to 2048 px) or of a PDF’s first page (up to 1600 px), or a JPEG of a video’s poster frame. Previews count toward the image preview limits.
- The text is the first 1 MB of the file’s text as UTF-8; `truncated` says it reached that limit. The first request has the text made and waits up to 10 seconds; until it is ready the answer is `503` `timed_out` with `Retry-After`.
- A PDF over 200 MB has no text: `422` `unsupported_content`. A PDF of scans has an empty text. A PDF that asks for a password has no pages, text or preview.

### Describe a file a note holds

`GET /api/integrations/v1/notes/{note_id}/files/{file_id}`

What the file is: its name, kind, type, size and media details, and where to read its text and preview. Any file the note's document holds, its images included. Originals are not available through connections. The note must be readable; it may be in Trash.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `read_note_file` |

**Parameters**

| Name | In | Type |
|---|---|---|
| `file_id` | path | uuid, required |
| `note_id` | path | uuid, required |

Answer `200`.

**Answer fields**

| Field | Type | Description |
|---|---|---|
| `id` | uuid, required |  |
| `name` | string, required | The name it goes by, with the extension of what it is. |
| `kind` | `image`, `video`, `audio`, `pdf`, `text`, `document`, `archive`, `other`, required |  |
| `media_type` | string, required | Found in the bytes. |
| `bytes` | integer, required |  |
| `width` | integer or null, required | Images and video. |
| `height` | integer or null, required |  |
| `media_info` | object or  or null or null, required | Duration and codecs; a PDF's pages. |
| `created_at` | date-time, required |  |
| `preview_url` | uri or null, required | An image's, a PDF's first page's or a video's ready preview. |
| `text_url` | uri or null, required | A text file's or a PDF's text. |

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID/files/$FILE_ID" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404, `unsupported_content` 413, 422, `attachment_blocked` 403, `attachment_checksum_mismatch` 409, `attachment_unavailable` 503.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

### Read the preview of a file a note holds

`GET /api/integrations/v1/notes/{note_id}/files/{file_id}/preview`

The ready preview, as it is: a WebP of an image (up to 2048 px) or of a PDF's first page (up to 1600 px), or a JPEG of a video's poster frame, without the original's metadata. Other files have none. Previews count toward the same limits as image previews: 20 a minute and 500 a day for each connection.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `read_note_file` |

**Parameters**

| Name | In | Type |
|---|---|---|
| `file_id` | path | uuid, required |
| `note_id` | path | uuid, required |

Answer `200`: The preview.

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID/files/$FILE_ID/preview" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404, `unsupported_content` 413, 422, `attachment_blocked` 403, `attachment_checksum_mismatch` 409, `attachment_unavailable` 503.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

### Read the text of a text file or a PDF a note holds

`GET /api/integrations/v1/notes/{note_id}/files/{file_id}/text`

Up to the first 1 MB of the file's text, as UTF-8. The first request has the text made, waiting up to 10 seconds; until it is ready the answer is 503 timed_out with Retry-After, and later requests answer it at once. A PDF of scans has an empty text; truncated says the text reached the 1 MB limit.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `read_note_file` |

**Parameters**

| Name | In | Type |
|---|---|---|
| `file_id` | path | uuid, required |
| `note_id` | path | uuid, required |

Answer `200`.

**Answer fields**

| Field | Type | Description |
|---|---|---|
| `id` | uuid, required |  |
| `text` | string, required | UTF-8; empty when the file holds none. |
| `truncated` | boolean, required | The text reached the 1 MB limit (a text of exactly that size counts too). |

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID/files/$FILE_ID/text" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404, `unsupported_content` 413, 422, `attachment_blocked` 403, `attachment_checksum_mismatch` 409, `attachment_unavailable` 503.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Move a note to Trash and restore it

Trash keeps the note; restoring brings it back where it was. When its thread is in Trash too, restore the thread first. Connections never delete permanently.

A note whose request is unfinished stays where it is: `409` `request_open`.

### Move a note to Trash

`POST /api/integrations/v1/notes/{note_id}/trash`

The MCP tool trash_note; restore brings it back. Read and add, for the notes it added; Edit and organize, any. Scope, shared places (shared_place_write) and the open-request rule (request_open) apply to every write.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `trash_note` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |

Answer `200`. Its fields are those under [Replace a note's content](#replaceNoteContent).

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/notes/$NOTE_ID/trash" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP"
```

Errors: `edit_not_allowed` 403, `unavailable` 404, `operation_id_reused` 409, `request_note_locked` 409, `request_open` 409, `shared_place_write` 409, `entity_deleted` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

### Restore a note

`POST /api/integrations/v1/notes/{note_id}/restore`

The MCP tool restore_note. When its thread is in Trash too, restore the thread first. Read and add, for the notes it added; Edit and organize. Scope, shared places (shared_place_write) and the open-request rule (request_open) apply to every write.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `restore_note` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |

Answer `200`. Its fields are those under [Replace a note's content](#replaceNoteContent).

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/notes/$NOTE_ID/restore" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP"
```

Errors: `edit_not_allowed` 403, `unavailable` 404, `operation_id_reused` 409, `request_open` 409, `shared_place_write` 409, `parent_deleted` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Restore an earlier version

Versions are kept for 90 days after they were replaced. When a connection changed a note, the last version a person wrote and the note’s first version stay until the note is deleted permanently.

1. `GET /notes/{note_id}/versions` lists them. `history_starts_at` and `oldest_available_at` say how far back the history reaches.
2. `GET /notes/{note_id}/versions/{version_id}` reads one. Note references in an old version are not followed.
3. `POST /notes/{note_id}/versions/{version_id}/restore` with the `content_revision` and `context_receipt` of a fresh `GET /notes/{note_id}` makes it current as a new revision. A note that changed since answers `409` `preview_changed`: read it again.

### List a note's earlier versions

`GET /api/integrations/v1/notes/{note_id}/versions`

Versions still kept: 90 days after they were replaced; a note a connection changed keeps the last version a person wrote and its first version until it is purged. Read only and up.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `get_note_history` |

**Parameters**

| Name | In | Type | Description |
|---|---|---|---|
| `cursor` | query | string | From next_cursor. |
| `note_id` | path | uuid, required |  |

Answer `200`.

**Answer fields**

| Field | Type |
|---|---|
| `items` | array of object, required |
| `next_cursor` | string or null, required |
| `history_starts_at` | date-time or null, required |
| `oldest_available_at` | date-time or null, required |

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID/versions" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404, `context_changed` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

### Read one earlier version

`GET /api/integrations/v1/notes/{note_id}/versions/{version_id}`

The version's text, Markdown and document beside the note's current content revision. Note references in an old version are not followed. Read only and up.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add · Read only |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| MCP | `get_note_version` |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |
| `version_id` | path | uuid, required |

Answer `200`.

**Answer fields**

| Field | Type |
|---|---|
| `version` | object, required |
| `text` | string, required |
| `markdown` | string, required |
| `document` | object, required |
| `current_content_revision` | integer, required |

```bash
curl -m 30 "https://thread.page/api/integrations/v1/notes/$NOTE_ID/versions/$VERSION_ID" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN"
```

Errors: `read_not_allowed` 403, `unavailable` 404.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

### Make an earlier version current

`POST /api/integrations/v1/notes/{note_id}/versions/{version_id}/restore`

The MCP tool restore_note_version: a new revision, the history in between is kept. Send the content_revision and context_receipt you read; a note that changed since answers 409 preview_changed. Read and add, for the notes it added; Edit and organize. Scope, shared places (shared_place_write) and the open-request rule (request_open) apply to every write.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| Revision | `content_revision` and `context_receipt` from `GET /notes/{note_id}`, in the body. |
| MCP | `restore_note_version` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |
| `version_id` | path | uuid, required |

**Body (application/json)**

| Field | Type | Description |
|---|---|---|
| `content_revision` | integer, required | The content_revision you read in full. |
| `context_receipt` | string, required | From the GET /notes/{note_id} you read. |

Answer `200`. Its fields are those under [Replace a note's content](#replaceNoteContent).

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/notes/$NOTE_ID/versions/$VERSION_ID/restore" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -H "Content-Type: application/json" \
  -d '{
  "content_revision": 5,
  "context_receipt": ".eJyrVkrLz1eyUspNzMlRKs7PTVWyUsrJLFKqBQBKxwbR:1vQx2a:Hk3mQ0Zp8Wc5T1nL4yJr2aVbE9sDf6gUo7iXqRb1sM"
}'
```

Errors: `edit_not_allowed` 403, `unavailable` 404, `invalid_json` 400, `unsupported_media_type` 415, `reread_required` 409, `preview_changed` 409, `operation_id_reused` 409, `request_note_locked` 409, `request_open` 409, `shared_place_write` 409, `thread_archived` 409, `entity_deleted` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Pin a note or mark it done

Send only what changes: `{"pinned": true}` or `{"done": false}`. Edit and organize only.

### Pin a note or mark it done

`PATCH /api/integrations/v1/notes/{note_id}`

The MCP tool update_note_properties. Edit and organize. Scope, shared places (shared_place_write) and the open-request rule (request_open) apply to every write.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `update_note_properties` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |

**Body (application/json)**

| Field | Type |
|---|---|
| `pinned` | boolean |
| `done` | boolean |

Answer `200`. Its fields are those under [Replace a note's content](#replaceNoteContent).

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X PATCH "https://thread.page/api/integrations/v1/notes/$NOTE_ID" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -H "Content-Type: application/json" \
  -d '{"pinned":true}'
```

Errors: `organize_not_allowed` 403, `unavailable` 404, `invalid_json` 400, `unsupported_media_type` 415, `operation_id_reused` 409, `request_note_locked` 409, `request_open` 409, `shared_place_write` 409, `thread_archived` 409, `entity_deleted` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## Move a note

Moving keeps the same note: its ID, history, pin and images go with it. The target must be a live, unarchived thread within the connection’s scope. Read and add moves the notes it added; Edit and organize, any.

### Move a note to another thread

`POST /api/integrations/v1/notes/{note_id}/move`

The MCP tool move_note: the same note, its id, history, pin and images go with it. Read and add, for the notes it added; Edit and organize. Scope, shared places (shared_place_write) and the open-request rule (request_open) apply to every write.

**Requirements**

| Requirement | Value |
|---|---|
| Levels | Edit and organize · Read and add |
| Plan | Pro with Sync. On Free, add to the Inbox instead (`POST /inbox/notes`). |
| Idempotency | `Operation-Id` header: one UUID per logical write, the same on every retry. |
| MCP | `move_note` |
| Shared threads | Only where the owner chose **Allow here**. |

**Parameters**

| Name | In | Type |
|---|---|---|
| `note_id` | path | uuid, required |

**Body (application/json)**

| Field | Type | Description |
|---|---|---|
| `thread_id` | uuid, required | A live, unarchived thread within this connection's content. |

Answer `200`. Its fields are those under [Replace a note's content](#replaceNoteContent).

```bash
OP=$(uuidgen)
curl -m 30 --retry 3 --retry-all-errors -X POST "https://thread.page/api/integrations/v1/notes/$NOTE_ID/move" \
  -H "Authorization: Bearer $THREADPAGE_TOKEN" \
  -H "Operation-Id: $OP" \
  -H "Content-Type: application/json" \
  -d '{"thread_id":"0e7c4b2a-91d3-4f6e-8a25-3c1b9d7f4e60"}'
```

Errors: `edit_not_allowed` 403, `unavailable` 404, `invalid_json` 400, `unsupported_media_type` 415, `operation_id_reused` 409, `request_note_locked` 409, `request_open` 409, `shared_place_write` 409, `thread_archived` 409, `entity_deleted` 409, `target_deleted` 409.

Also [the errors of every call](https://thread.page/help/connections/errors.md#errors-every-call-can-get), and `pro_required` or `cloud_required` without Pro and Sync.

## In shared threads

In a thread the owner shares with other people, a connection that reads adds, changes and moves notes only where the owner allowed it. This covers every note write: adding, images, edits, checklist ticks, pin, Done, moves into or out of the thread, Trash, restore and version restores. Otherwise the write answers `409` `shared_place_write`. See [Shared threads and folders](https://thread.page/help/connections/api.md#shared-threads-and-folders).

## If it fails

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `stale_block` | 409 | Someone changed the same part of the note since your read. | Read the note again and apply your change again. | No |
| `reread_required` | 409 | The read your edit is based on is out of date, or belongs to another connection. | Read the note again and apply your change again. | No |
| `lossy_markdown` | 400 | Markdown or text would drop formatting the note holds. | Read the note and send its document with your change. | No |
| `operation_id_reused` | 409 | The Operation-Id (MCP: operation_id) was already used with another body. | If you are repeating a lost write, read GET /operations/{id} first. Otherwise use a new ID. | No |
| `edit_not_allowed` | 403 | Changing a note this connection did not add, or a reminder it did not set, on Read and add; any change below Read and add. | Add a new note instead, or give the person details.manage_url. | No |
| `organize_not_allowed` | 403 | Threads, folders, pin or Done below Edit and organize. | Give the person details.manage_url. | No |
| `request_note_locked` | 409 | A note written for a request: it never changes. | Add a new note instead. | No |
| `shared_place_write` | 409 | A thread or folder shared with people where the owner has not allowed this connection to write; a request there ends with it before its run. | Tell the owner: they choose Allow here in ThreadPage. | No |
| `request_open` | 409 | The note, or a note in the thread or folder, is with a recipient. | Wait until its request ends, or ask the owner. | No |
| `preview_changed` | 409 | The note changed since the read a version restore is based on. | Read the note again, then restore. | No |
| `content_too_large` | 413 | A note over 256 KiB, Markdown over 32 KiB, an image or an MCP file over 5 MB, or a body over 1 MiB. | Split the note, or send a smaller image. | No |
| `unsupported_content` | 413, 422 | A document over 256 KiB, or a file or image with no preview or text. | Split the note; for a file, tell the person. | No |
| `invalid_content` | 400 | Markdown or text the converter can't take, such as too many lines. | Fix the content or split it into several notes. | No |
| `invalid_document` | 400 | A document that breaks the RichDocument v4 schema. | Fix the document; the formatting guide lists every element. | No |
| `image_unavailable` | 409 | The note names an image this connection may not use there. | Upload the image to that thread first. | No |
| `invalid_attachment` | 422 | Not a PNG, JPEG or WebP image within 5 MB, 20 megapixels and 300 frames. | Send a supported image. | No |
| `attachment_quota_exceeded` | 413 | The workspace's storage is full. | Tell the person. | No |
| `image_busy` | 503 | Another image is being processed. | Wait Retry-After seconds and send the same request again. | Yes |
| `thread_archived` | 409 | The thread is archived, so it takes no new notes. | Unarchive it first, or ask the owner. | No |
| `entity_deleted` | 409 | The item is in Trash. | Restore it first, or ask the owner. | No |
| `parent_deleted` | 409 | Its thread or folder is in Trash. | Restore the thread or folder first. | No |
| `target_deleted` | 409 | The destination is in Trash. | Restore it, or choose another destination. | No |
| `quota_exhausted` | 429 | Free's 3,000 connection notes this month are used. | Stop until details.resets_at. There is no Retry-After. | No |
| `inbox_full` | 409 | 1,000 notes or 20 MB wait for the owner's browser to collect them. | Ask the owner to open ThreadPage, then retry later with the same Operation-Id. | No |
| `pro_required` | 403 | It needs Pro: on Free a connection adds notes only to the Inbox. | Use POST /inbox/notes (MCP: create_note without thread_id), or tell the owner. | No |

## Next

- [Threads and folders](https://thread.page/help/connections/api/threads.md)
- [Events and subscriptions](https://thread.page/help/connections/events.md): hear when notes change.
