# Errors

Every refusal has a stable `code` you can branch on.

## Error shape

### Integration API

```json
{
  "code": "stale_block",
  "message": "Someone changed the same part of the note. Read it again.",
  "retryable": false,
  "diagnostic_id": "9d2e6c41-3b7a-4f05-8e1d-6a0c2f9b5e73",
  "details": {
    "content_revision": 8,
    "current": [{"type": "paragraph", "content": [{"type": "text", "text": "Ship on Monday."}]}]
  }
}
```

`details.current` holds the current content of each part that both you and someone else changed.

- `code`: stable and machine-readable. Branch on it, never on `message`.
- `message`: a short explanation for a person. Its wording can change.
- `retryable`: `true` when the same request may succeed later. Never a reason to repeat a write with a new `Operation-Id`.
- `diagnostic_id`: quote it to support.
- `details`: more about some codes, listed with each code below.

`429` and `503` also carry a `Retry-After` header in seconds.

### MCP server

A refused tool call answers a result with `isError: true` and the same codes, as JSON in its text content:

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "isError": true,
    "content": [
      {
        "type": "text",
        "text": "{\"status\": \"rejected\", \"code\": \"stale_block\", \"message\": \"Someone changed the same part of the note. Read it again.\", \"details\": {\"content_revision\": 8, \"current\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"text\", \"text\": \"Ship on Monday.\"}]}]}}"
      }
    ]
  }
}
```

The JSON is `{"status": "rejected", "code", "message", "details"}`: it has no `retryable` and no `diagnostic_id`. A rate limit carries `details.retry_after`. A scope miss is `unavailable` on MCP, where the API answers `404`.

### Endpoint URLs

`POST /in/{capability}` answers with `outcome` (`accepted`, `duplicate`, `ignored`, `blocked`, `failed` or `rejected`) and a `diagnostic_id`, which is also the `X-Request-ID` header. A refusal adds `code` and `message`. See [Endpoint URLs](https://thread.page/help/connections/endpoints.md#answers).

## Diagnostic ID

Every API and endpoint URL error carries a `diagnostic_id`. Quote it when you contact support at https://thread.page/contact or support@thread.page.

[Contact support](https://thread.page/contact?type=question&source=help_article&page=help_connections)

## How to handle

| Status | Means | What to do |
|---|---|---|
| `400` | The request is malformed: bad JSON, content in no supported form, a token in the query string, or `lossy_markdown`. | Fix the request. Sending it unchanged fails again. |
| `401` | The token is missing, wrong, revoked, replaced or retired, or its term ended. | For `token_expired`, ask the owner to choose **Renew**. Otherwise ask the owner for a new token. |
| `403` | The level, the plan or the owner’s settings don’t allow it. | Read `code`. For a level refusal, give the owner `details.manage_url`. |
| `404` | Not found within the connection’s scope. | Don’t guess other IDs. Ask the owner whether the item is in scope. |
| `409` | A conflict with the current state. | Read `code`: some need a fresh read, some a confirmation, some the owner. |
| `410` | A pull feed cursor expired. | Continue from `details.resync_cursor`. |
| `413` | The content or the body is too large, or the workspace’s storage is full. | Split the note, or send a smaller image. For `attachment_quota_exceeded`, tell the person. |
| `415` | Not sent as JSON (or, for an image, as `multipart/form-data`). | Fix the `Content-Type`. |
| `422` | A parameter or header has an unsupported value, or a write has no `Operation-Id`. | Fix what the message names. |
| `429` | A rate limit or the monthly Free quota. | Wait `Retry-After` seconds and retry with the same `Operation-Id`. `quota_exhausted` has no `Retry-After`: stop until `details.resets_at`. |
| `503` | ThreadPage is busy or the call took too long. | Wait `Retry-After` seconds and retry with the same `Operation-Id`. |

## Errors every call can get

Any call with a token can answer these, whatever it does. Each operation lists its own codes under its block.

**Errors every call can get**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `authentication_required` | 401 | No token was sent. | Send Authorization: Bearer tp_…. | No |
| `invalid_token` | 401 | The token is wrong, revoked or replaced, or it was retired: the owner changed their password, or reset or restored the workspace. /mcp also answers it for a token that only answers its connection's requests. | Ask the owner for a new token. | No |
| `token_expired` | 401, 409 | The token's term ended, or it went 90 days without a call: details.reason is term or inactivity, details.expired_at when. An endpoint URL answers 409 with outcome blocked. | Ask the owner to renew it in Settings → Connections; the same token then works again. | No |
| `invalid_request` | 400, 422 | A parameter, field or header is missing or has an unsupported value (a write without Operation-Id too), or the token was sent in the query string. | Fix what the message names. Sending it unchanged fails again. | No |
| `rate_limited` | 429 | A technical limit or a change limit; details.reason names it and details.scope whose it is. | Wait Retry-After seconds (MCP: details.retry_after) and send the identical request. | Yes |
| `timed_out` | 503 | The call took longer than 10 seconds, or a file's text or preview is still being made. | Wait Retry-After seconds and send the identical request. | Yes |
| `database_busy` | 503 | ThreadPage is busy. | Wait Retry-After seconds and send the identical request. | Yes |

## All codes

Each code has its own anchor, `#error-<code>`, so you can link to it.

### Authentication

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `authentication_required` | 401 | No token was sent. | Send Authorization: Bearer tp_…. | No |
| `invalid_token` | 401 | The token is wrong, revoked or replaced, or it was retired: the owner changed their password, or reset or restored the workspace. /mcp also answers it for a token that only answers its connection's requests. | Ask the owner for a new token. | No |
| `token_expired` | 401, 409 | The token's term ended, or it went 90 days without a call: details.reason is term or inactivity, details.expired_at when. An endpoint URL answers 409 with outcome blocked. | Ask the owner to renew it in Settings → Connections; the same token then works again. | No |
| `oauth_required` | 403 | A token on Edit and organize was sent to /mcp, which takes that level only through OAuth; /mcp answers it before any method, the Integration API keeps the level. | Connect the assistant to /mcp and sign in instead of using the token, or ask the owner to lower the token's level in Settings → Connections. | No |
| `reconnect_required` | 403, 409 | An assistant's sign-in ended or was retired (the owner changed their password, or reset or restored the workspace); at an endpoint URL, its connection's token was retired (409 with outcome blocked). | The owner reconnects the assistant, or replaces the custom connection's token. | No |

### Access

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `read_not_allowed` | 403 | Reading, or subscribing to events, on Add only or Add to Inbox. | Tell the person and give them details.manage_url. | No |
| `write_not_allowed` | 403, 409 | Adding on Read only, or receiving requests below Read and add (details.needs and details.manage_url); an assistant whose sign-in holds no write scope; a connection whose access leaves out the Inbox thread. An endpoint URL answers 409 with outcome blocked. | Give the person details.manage_url where there is one; otherwise tell them. | 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 |
| `unavailable` | 404 | Not found within this connection's scope. | Don't guess other IDs; ask the person whether the item is in scope. | No |
| `not_found` | 404 | No such request, subscription, delivery, endpoint, reminder or operation for this connection. | Check the ID; it may have expired or been deleted. | No |
| `connection_unavailable` | 409 | The connection can't act now: its token is not current, or its state stops it. | Tell the owner. | No |
| `connection_paused` | 409, 503 | The owner paused this connection: every call but GET /connection (get_access) and GET /operations/{id} (get_operation) waits for Resume, which keeps its token. An endpoint URL answers 503 and keeps the event's receipt. | Tell the person they can resume it in Settings → Connections (details.manage_url); don't retry until then. An endpoint's sender retries. | No |

A level refusal carries `details.needs`, the `level` value the call needs, and `details.manage_url`, where the owner changes only the level.

### Content

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `invalid_request` | 400, 422 | A parameter, field or header is missing or has an unsupported value (a write without Operation-Id too), or the token was sent in the query string. | Fix what the message names. Sending it unchanged fails again. | No |
| `invalid_json` | 400 | The body is not a JSON object, or it nests too deep. | Send a JSON object. | 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 |
| `invalid_link` | 400 | A thread description link without href, or with a label that is not text or null. | Send {href, label}. | No |
| `invalid_rule` | 400 | A reminder rule ThreadPage can't take, or a one-time reminder already past. | Fix the rule as the reminder routes describe it. | No |
| `lossy_markdown` | 400 | Markdown or text would drop formatting the note holds. | Read the note and send its document with your change. | 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 |
| `unsupported_media_type` | 415 | Not sent as JSON (an image: not multipart/form-data). | Fix the Content-Type. | No |

### Conflicts

**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 |
| `context_changed` | 409 | A list cursor is older than an hour, or the owner changed the connection's access since. | Start the list again without a cursor. | No |
| `preview_changed` | 409 | The note changed since the read a version restore is based on. | Read the note again, then restore. | 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 |
| `request_note_locked` | 409 | A note written for a request: it never changes. | Add a new note instead. | 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 |
| `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 |
| `confirmation_required` | 409 | Moving a folder, moving it to Trash or deleting it alone when it holds threads. | Show the counts to the person, then send the same call with details.confirm within 10 minutes. | No |
| `declined` | MCP only | The person did not accept the confirmation question. | Don't repeat the call; ask the person. | No |
| `reminder_exists` | 409 | The person already has a reminder there and replace was not set. | Send replace true only when the person asked to change it. | No |

### State

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `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 |
| `folder_cycle` | 409 | A folder can't move inside itself. | Choose another destination. | No |
| `folder_name_conflict` | 409 | A folder next to it already has this name. | Choose another name. | No |

### Images and files

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `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 |
| `attachment_blocked` | 403 | The operator blocked this file. | Tell the person. | No |
| `attachment_checksum_mismatch` | 409 | The stored original does not match its checksum. | Tell the person; quote the diagnostic_id to support. | No |
| `attachment_unavailable` | 503 | A file's bytes can't be read now. | Wait Retry-After seconds and send the same request again; if it stays, tell the person. | Yes |
| `attachments_unavailable` | 503 | File storage is unavailable for a moment. | Wait Retry-After seconds and send the same request again. | Yes |
| `image_busy` | 503 | Another image is being processed. | Wait Retry-After seconds and send the same request again. | Yes |

### Plan and quota

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `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 |
| `cloud_required` | 403 | It needs the owner's cloud copy: Pro without Sync. | Tell the owner to turn on Sync; adding to the Inbox works meanwhile. | No |
| `entitlement_required` | 403 | The owner's Pro ended while the call ran. | Tell the owner; adding to the Inbox works on Free. | 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 |

### Limits

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `rate_limited` | 429 | A technical limit or a change limit; details.reason names it and details.scope whose it is. | Wait Retry-After seconds (MCP: details.retry_after) and send the identical request. | Yes |
| `reminders_limit` | 422 | The thread, or the person, has as many reminders as they may (details.scope). | Tell the person. | No |
| `timed_out` | 503 | The call took longer than 10 seconds, or a file's text or preview is still being made. | Wait Retry-After seconds and send the identical request. | Yes |
| `database_busy` | 503 | ThreadPage is busy. | Wait Retry-After seconds and send the identical request. | Yes |

### Subscriptions

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `webhooks_not_allowed` | 403 | The owner has not turned on Let the script add note-update URLs through the API. | Use a pull feed, or ask the owner. | No |
| `content_not_allowed` | 403 | A token asked for payload_mode content, or for origins other than human, for a subscription or a sample. | Read note text by ID, or ask the owner to add the webhook in ThreadPage. | No |
| `invalid_endpoint` | 422 | The webhook URL may not receive deliveries. | Use a public https URL. | No |
| `subscription_limit` | 409 | The connection already has 10 webhooks and pull feeds. | Delete one first. | No |
| `webhook_limit` | 409 | The account already has 50 live webhooks. | Delete one first, or ask the owner. | No |
| `subscription_paused` | 409 | The subscription is paused. | Resume it first. | No |
| `subscription_expired` | 409 | The webhook's endpoint answered 410, so it stopped. | Create a new subscription. | No |
| `paused_by_owner` | 409 | The owner paused it, so only the owner resumes it. | Ask the owner. | No |
| `not_retryable` | 409 | Only the latest failed delivery of an event can be sent again. | Retry that delivery instead. | No |
| `content_removed` | 409 | The event's content is no longer kept, so its delivery can't be sent again. | Read the note through the API instead. | No |
| `delivery_limit` | 409 | Resuming while the account's deliveries for the day are used. | Wait until after midnight UTC. | No |
| `cursor_expired` | 410 | A pull feed cursor older than 7 days, or from another feed. | Continue from details.resync_cursor and read current notes through the API. | No |

### Requests

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `not_receiving` | 403 | Receiving requests is off for this connection. | Ask the owner to switch on Receive requests. | No |
| `already_claimed` | 409 | Another run claimed the request with a different claim_id. | Don't start the work. | No |
| `not_claimed` | 409 | A report or result before the request was claimed. | Claim it first. | No |
| `cancellation_requested` | 409 | The owner asked to stop. | Send status cancelled (or outcome_unknown), or the result if the work is done. | No |
| `request_closed` | 409 | The request has ended: completed, cancelled, failed, expired or with an unknown outcome. | Stop; nothing more is accepted. | No |

### Endpoint URLs

**Errors**

| Code | HTTP | When | What to do | Retryable |
|---|---|---|---|---|
| `invalid_signature` | 401 | The signature or its webhook-id is missing, wrong or older than 5 minutes. | Sign the raw body with the endpoint's secret, with a fresh timestamp. | No |
| `secret_unavailable` | 401 | The endpoint can no longer check signatures. | Ask the owner to add the endpoint again. | No |
| `method_not_allowed` | 405 | Not sent with POST. | Send POST. | No |
| `endpoint_paused` | 503 | The owner paused the endpoint; the event is not kept. | Ask the owner to resume it, then send the event again. | Yes |
| `missing_field` | 422 | The event lacks a field the template needs. | Send the field, or mark it optional with ? in the template. | No |
| `empty_note` | 422 | The event makes an empty note. | Send text or markdown. | No |

## Next

- [Rate limits](https://thread.page/help/connections/limits.md): every `details.reason` of a `429`.
- [Integration API basics: writing safely](https://thread.page/help/connections/api.md#writing-safely)
