# HTTP API reference

> Editor and agent HTTP routes on the Hocuspocus server (`/api/agent-write-md`, `/api/search`, `/api/documents`, `/api/share/*`, `/api/local-op/*`) used by the web app and desktop shell.

- Repository: sashimikun/open-knowledge
- GitHub: https://github.com/sashimikun/open-knowledge
- Human docs: https://grok-wiki.com/public/docs/sashimikun-open-knowledge-5c45105c876e
- Complete Markdown: https://grok-wiki.com/public/docs/sashimikun-open-knowledge-5c45105c876e/llms-full.txt

## Source Files

- `packages/server/src/api-extension.ts`
- `packages/server/src/api-config.test.ts`
- `packages/server/src/api-agent-write-summary.test.ts`
- `packages/server/src/api-search.test.ts`
- `packages/server/src/share/publish.ts`
- `packages/server/src/share/construct-url.ts`
- `packages/server/src/api-extension.ok-init.test.ts`

---

---
title: "HTTP API reference"
description: "Editor and agent HTTP routes on the Hocuspocus server (`/api/agent-write-md`, `/api/search`, `/api/documents`, `/api/share/*`, `/api/local-op/*`) used by the web app and desktop shell."
---

The collaboration server registers a Hocuspocus extension from `createApiExtension` (`packages/server/src/api-extension.ts`). Its `onRequest` hook (priority 100) dispatches every `/api/*` path before static file serving. Request and response bodies are validated against Zod schemas exported from `@inkeep/open-knowledge-core` (`packages/core/src/schemas/api/`). The web editor, desktop shell, and desktop bridge call these routes on the loopback host bound by `ok start`.

```mermaid
flowchart LR
  subgraph clients [Clients]
    Web["Web editor"]
    Desktop["Desktop shell"]
    Bridge["Desktop bridge"]
  end
  subgraph server [Hocuspocus server]
    Gate["Origin + loopback gates"]
    Dispatch["Route table"]
    Yjs["Yjs / agent sessions"]
    FS["Content filesystem"]
    CLI["ok CLI subprocess"]
  end
  Web --> Gate
  Desktop --> Gate
  Bridge --> Gate
  Gate --> Dispatch
  Dispatch --> Yjs
  Dispatch --> FS
  Dispatch --> CLI
```

## Base URL and transport

| Property | Value |
| --- | --- |
| Host | Loopback only: `localhost`, `127.x.x.x`, or `[::1]` with optional port |
| Scheme | `http://` (WebSocket collab is separate at `ws://<host>/collab`) |
| Bootstrap | `GET /api/config` returns `collabUrl`, `port`, and optional `paneTarget` |
| CORS | `OPTIONS` preflight supported; `Access-Control-Allow-Origin` echoes a permitted loopback `Origin` |
| Client headers | Optional `x-ok-client-protocol`, `x-ok-client-runtime`, `x-ok-client-kind` (see `CLIENT_VERSION_HEADER` in `@inkeep/open-knowledge-core`) |

<Note>
`GET /api/config` builds `collabUrl` as `ws://<Host>/collab`. When the `Host` header is absent, `collabUrl` is `null` by design.
</Note>

## Security gates

All `/api/*` requests pass an origin gate: if `Origin` is present, the hostname must be `localhost`, `::1`, `[::1]`, or `127.*.*.*` (`packages/server/src/api-origin.ts`).

**Mutating routes** (writes, uploads, sync triggers, git checkout, and every `/api/local-op/*` path) additionally require:

- Peer address on loopback (`127.*`, `::ffff:127.*`, or `::1`)
- `Host` header matching `isAllowedWorkspaceHostHeader` (localhost or 127.x only)

In **ephemeral single-file mode**, every `/api/*` request is loopback-gated.

`/api/local-op/*` handlers also call `checkLocalOpSecurity`: loopback peer plus a loopback `Origin` when one is sent (`packages/server/src/local-op-security.ts`).

## Response formats

**Success** — `application/json` body validated against the route’s success schema.

**Error** — `application/problem+json` with RFC 7807-style fields:

<ResponseField name="type" type="string">
URN problem type, e.g. `urn:ok:error:invalid-request`, `urn:ok:error:loopback-required`, `urn:ok:error:not-found`. Full enum in `ProblemTypeSchema`.
</ResponseField>

<ResponseField name="title" type="string">
Human-readable summary.
</ResponseField>

<ResponseField name="status" type="number">
HTTP status code (400–599).
</ResponseField>

<ResponseField name="detail" type="string">
Optional extra context.
</ResponseField>

**Streaming** — Some routes return `application/x-ndjson` (clone, auth login, document list `showAll` stream). Terminal error lines use `{ type: "error", problem: { ... } }`.

## Route inventory

Paths are exact unless noted. Methods not listed return `405` with `Allow` when the handler enforces method.

### Bootstrap and workspace

| Method | Path | Purpose |
| --- | --- | --- |
| `GET`, `HEAD` | `/api/config` | Editor bootstrap: collab URL, port, pane target, single-file flag |
| `DELETE` | `/api/config` | One-shot consume of armed `paneTarget` (loopback + host gate) |
| `GET` | `/api/server-info` | Server instance ID, branch, disk-ack state |
| `GET` | `/api/workspace` | Resolved content directory path (loopback) |
| `GET` | `/api/principal` | Git-configured principal (loopback) |

### Documents and listing

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/document` | Read one open or on-disk document by `docName` |
| `GET` | `/api/documents` | List documents, assets, folders under optional `dir` |
| `GET` | `/api/pages` | Flat page index for sidebar |
| `GET` | `/api/asset` | Serve binary asset by content-relative `path` |
| `GET` | `/api/asset-text` | Serve text asset content |

### Agent writes (server-routed)

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/agent-write-md` | Primary markdown write into Yjs + disk |
| `POST` | `/api/agent-write` | Legacy plain-text write |
| `POST` | `/api/agent-patch` | Find/replace patch on document body |
| `POST` | `/api/frontmatter-patch` | Patch YAML frontmatter keys |
| `POST` | `/api/agent-undo` | Undo agent edit stack |
| `GET` | `/api/agent-activity` | List agent activity for a document |
| `GET` | `/api/agent-burst-diff` | Burst diff for agent stack item |

### Search and tags

| Method | Path | Purpose |
| --- | --- | --- |
| `GET`, `POST` | `/api/search` | Workspace search (omnibar, full-text, autocomplete) |
| `GET` | `/api/semantic-status` | Embeddings index status |
| `GET` | `/api/suggest-links` | Link suggestions for a document |
| `GET` | `/api/tags` | Tag index with counts |
| `GET` | `/api/tags/:name` | Documents carrying a tag (URL-encoded slashes) |

### Link graph

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/backlinks` | Incoming links for `docName` |
| `GET` | `/api/backlink-counts` | Counts for comma-separated `docNames` |
| `GET` | `/api/forward-links` | Outgoing links from `docName` |
| `GET` | `/api/link-graph` | N-degree neighborhood (`degrees`, optional `docName`) |
| `GET` | `/api/dead-links` | Broken links (optional `sourceDocName` filters) |
| `GET` | `/api/orphans` | Orphan pages (`mode`: `both`, `incoming`, `outgoing`) |
| `GET` | `/api/hubs` | High in-degree pages (`limit`) |

### File operations

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/create-page` | Create markdown page |
| `POST` | `/api/create-folder` | Create folder |
| `POST` | `/api/duplicate-path` | Duplicate file or folder |
| `POST` | `/api/rename-path` | Rename/move path |
| `POST` | `/api/delete-path` | Delete path |
| `POST` | `/api/trash/cleanup` | Permanently remove trashed items |
| `POST` | `/api/upload` | Stream upload asset (multipart) |

### History and versions

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/history` | List versions for `docName` |
| `GET` | `/api/history/:sha` | Fetch one version by SHA |
| `POST` | `/api/save-version` | Checkpoint current document |
| `POST` | `/api/rollback` | Roll back to a version |

### Share and git

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/share/construct-url` | Build `openknowledge.ai/d/…` share URL |
| `GET` | `/api/share/publish/owners` | List GitHub owners for publish UI |
| `GET` | `/api/share/publish/name-check` | Check repo name availability |
| `POST` | `/api/share/publish` | Create GitHub repo and push project |
| `GET` | `/api/git/branch-info` | Local branch metadata |
| `POST` | `/api/git/checkout` | Checkout branch |

### Sync

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/sync/status` | Sync engine status |
| `POST` | `/api/sync/trigger` | Trigger pull/push |
| `GET` | `/api/sync/conflicts` | List merge conflicts |
| `GET` | `/api/sync/conflict-content` | Conflict file content |
| `POST` | `/api/sync/resolve-conflict` | Resolve a conflict |

### Local operations (`/api/local-op/*`)

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/local-op/clone` | Clone repo, start server (NDJSON stream) |
| `POST` | `/api/local-op/ok-init` | Scaffold `.ok/` on a git worktree |
| `POST` | `/api/local-op/auth/login` | GitHub device-flow login (NDJSON) |
| `POST` | `/api/local-op/auth/status` | Auth status JSON |
| `POST` | `/api/local-op/auth/repos` | List accessible repos |
| `POST` | `/api/local-op/auth/signout` | Sign out |
| `POST` | `/api/local-op/auth/set-identity` | Set git identity |
| `POST` | `/api/local-op/embeddings/set-key` | Store embeddings API key |
| `POST` | `/api/local-op/embeddings/clear-key` | Clear embeddings key |

### Templates, folders, seeds, desktop helpers

| Method | Path | Purpose |
| --- | --- | --- |
| `GET`, `PUT`, `POST`, `DELETE` | `/api/template` | CRUD/move project templates |
| `GET` | `/api/templates` | List templates |
| `GET`, `PUT` | `/api/folder-config` | Folder frontmatter config |
| `GET` | `/api/page-headings` | Heading outline for a page |
| `POST` | `/api/install-skill` | Install agent skill |
| `GET` | `/api/skill/install-state` | Skill install snapshot |
| `GET` | `/api/seed/packs` | List starter packs |
| `GET` | `/api/seed/plan` | Plan seed application |
| `POST` | `/api/seed/apply` | Apply starter pack |
| `GET` | `/api/installed-agents` | Detect installed agent CLIs |
| `POST` | `/api/spawn-cursor` | Launch Cursor on project |
| `POST` | `/api/handoff` | Open-with-AI handoff dispatch |
| `POST` | `/api/client-logs` | Ingest renderer console logs |
| `GET` | `/api/rescue` | List rescue/timeline entries |
| `GET` | `/api/metrics/reconciliation` | Reconciliation metrics |
| `GET` | `/api/metrics/parse-health` | Parse health metrics |
| `GET` | `/api/metrics/agent-presence` | Live agent presence map |

<Info>
When `enableTestRoutes` is set, the server also exposes `/api/test-reset`, `/api/test-flush-git`, `/api/test-rescan-backlinks`, and `/api/test-rescan-files` for integration tests only.
</Info>

---

## `GET /api/config`

Desktop and web clients poll this route on startup.

:::endpoint GET /api/config
Returns the ok-ui-shaped bootstrap payload with same-origin `collabUrl`, bound port, and optional armed pane target.
:::

<ParamField body="(none)" type="—">
No query parameters. Uses request `Host` header to build `collabUrl`.
</ParamField>

<ResponseField name="collabUrl" type="string | null">
WebSocket URL `ws://<host>/collab`, or `null` when `Host` is missing.
</ResponseField>

<ResponseField name="previewUrl" type="null">
Always `null` on the collab server (preview URLs come from MCP `get_preview_url`).
</ResponseField>

<ResponseField name="port" type="number">
Bound server port from `.ok/local/server.lock`, or `0` when unconfigured.
</ResponseField>

<ResponseField name="paneTarget" type="string | null">
One-shot hash route (e.g. `#/folder/doc`) armed by CLI; cleared by `DELETE /api/config`.
</ResponseField>

<ResponseField name="singleFile" type="boolean">
`true` in ephemeral single-file sessions (`ok notes.md`).
</ResponseField>

<RequestExample>
```bash
curl -s http://127.0.0.1:7777/api/config
```
</RequestExample>

<ResponseExample>
```json
{
  "collabUrl": "ws://127.0.0.1:7777/collab",
  "previewUrl": null,
  "port": 7777,
  "paneTarget": null,
  "singleFile": false
}
```
</ResponseExample>

---

## Agent write routes

Agent HTTP writes mirror MCP `write` / `edit` but route through the collab server so Yjs, disk persistence, git flush, and contributor attribution stay consistent.

### `POST /api/agent-write-md`

:::endpoint POST /api/agent-write-md
Apply markdown to a document via Yjs, flush to disk and git, and return subscriber counts plus optional warnings.
:::

<ParamField body="docName" type="string" required>
Content-relative document name (no extension). Rejects reserved system/config names.
</ParamField>

<ParamField body="markdown" type="string" required>
Markdown body to write.
</ParamField>

<ParamField body="position" type="append | prepend | replace">
Default `append`. `replace` rewrites the full body.
</ParamField>

<ParamField body="extension" type=".md | .mdx | …">
Optional doc extension when creating a new file.
</ParamField>

<ParamField body="agentId" type="string" required>
Stable agent session identifier.
</ParamField>

<ParamField body="agentName" type="string" required>
Display name for presence and attribution.
</ParamField>

<ParamField body="summary" type="string">
Optional edit summary (max 80 chars; longer values truncated with `truncatedFrom` in response).
</ParamField>

<ResponseField name="timestamp" type="string">
ISO-8601 write timestamp.
</ResponseField>

<ResponseField name="subscriberCount" type="number">
Yjs subscribers on the document channel.
</ResponseField>

<ResponseField name="systemSubscriberCount" type="number">
Subscribers on the system observer channel.
</ResponseField>

<ResponseField name="summary" type="object">
`{ value, truncatedFrom?, hint? }` when a summary was provided.
</ResponseField>

<ResponseField name="warning" type="object">
Optional `content-divergence` or `disk-edit-reconciled` advisory.
</ResponseField>

<Warning>
Frontmatter cannot be changed via body find/replace on `/api/agent-patch`. Use `/api/frontmatter-patch` or `position: "replace"` on `/api/agent-write-md` for YAML block edits.
</Warning>

<RequestExample>
```bash
curl -s -X POST http://127.0.0.1:7777/api/agent-write-md \
  -H 'Content-Type: application/json' \
  -d '{
    "docName": "guides/quickstart",
    "markdown": "# Quickstart\n\nUpdated intro.\n",
    "position": "replace",
    "agentId": "cursor-1",
    "agentName": "Cursor",
    "summary": "Refresh quickstart intro"
  }'
```
</RequestExample>

Related legacy/surgical endpoints:

| Method | Path | Role |
| --- | --- | --- |
| `POST` | `/api/agent-write` | Legacy plain `content` write (parity with write-md for summaries) |
| `POST` | `/api/agent-patch` | Body `find` / `replace` surgical edit |
| `POST` | `/api/frontmatter-patch` | Patch frontmatter map with typed values |

---

## `GET` / `POST /api/search`

:::endpoint GET /api/search
Query the workspace search corpus with URL query parameters.
:::

:::endpoint POST /api/search
Same search engine with a JSON body (used by shared HTTP clients and MCP-backed flows).
:::

<ParamField body="query" type="string">
Search string. Max 200 characters; longer queries return `400`.
</ParamField>

<ParamField body="intent" type="autocomplete | full_text | omnibar">
Controls ranking tiers. `omnibar` returns page and folder entity matches; `full_text` includes body snippets.
</ParamField>

<ParamField body="ranking" type="navigation | relevance">
Reorders the same candidate set: `navigation` favors name matches; `relevance` favors body term density.
</ParamField>

<ParamField body="scopes" type="array">
Subset of `page`, `folder`, `content`, `file`. Also accepted as comma-separated `scope` query param on GET.
</ParamField>

<ParamField body="limit" type="number">
Maximum results (non-negative integer).
</ParamField>

<ParamField body="semantic" type="boolean">
Opt-in semantic reranking when embeddings are configured.
</ParamField>

<ParamField body="source" type="omnibar | mcp | http">
Telemetry source tag.
</ParamField>

<ResponseField name="results" type="array">
Entries with `kind`, `path`, `title`, `score`, `signals`, optional `snippet`.
</ResponseField>

<ResponseField name="elapsedMs" type="number">
Server-side search duration.
</ResponseField>

<ResponseField name="semantic" type="object">
When requested: `capable`, `applied`, `coverage: { embedded, total }`.
</ResponseField>

<ResponseField name="ready" type="boolean">
`false` while the file index boot gate is still warming (partial corpus).
</ResponseField>

<ResponseField name="truncated" type="boolean">
`true` when the name-only file tier hit `OK_SEARCH_MAX_ENTRIES`.
</ResponseField>

<RequestExample>
```bash
curl -s 'http://127.0.0.1:7777/api/search?query=arch&intent=omnibar'
```
</RequestExample>

<ResponseExample>
```json
{
  "query": "arch",
  "intent": "omnibar",
  "results": [
    { "kind": "folder", "path": "architecture", "title": "architecture", "score": 1, "signals": {} },
    { "kind": "page", "path": "architecture/overview", "title": "System Overview", "score": 0.9, "signals": {} }
  ],
  "elapsedMs": 12
}
```
</ResponseExample>

POST body limit is ~1 MiB; oversize bodies return `413` (`urn:ok:error:payload-too-large`). Malformed JSON returns `400`.

Pair with `GET /api/semantic-status` for embeddings key presence and index readiness.

---

## `GET /api/documents`

:::endpoint GET /api/documents
List documents, assets, folders, and non-markdown files under the content root.
:::

<ParamField body="dir" type="string">
Optional subdirectory filter (content-relative). Invalid paths return `400`.
</ParamField>

<ParamField body="showAll" type="boolean">
When `true`, include assets and non-markdown files subject to content filters.
</ParamField>

<ParamField body="depth" type="1">
With `showAll=true`, `depth=1` limits recursion to one level.
</ParamField>

<ResponseField name="documents" type="array">
`DocumentListEntry` objects: `kind` is `document`, `asset`, `folder`, or `file` with kind-specific fields.
</ResponseField>

<ResponseField name="truncated" type="boolean">
Present when the show-all walk hits the entry cap.
</ResponseField>

When `showAll=true` and the client sends `Accept: application/x-ndjson`, the handler streams one JSON object per line, ending with `{ "type": "complete", "truncated", "count" }`.

<RequestExample>
```bash
curl -s 'http://127.0.0.1:7777/api/documents?dir=guides'
```
</RequestExample>

For a single loaded document’s Yjs text, use `GET /api/document?docName=<name>`. A missing on-disk file returns `404` without creating a phantom Y.Doc.

---

## Share routes (`/api/share/*`)

Share endpoints read git remotes and invoke `ok share` subprocesses. Publish and owners routes require loopback security like other `/api/local-op/*` gates.

### `POST /api/share/construct-url`

:::endpoint POST /api/share/construct-url
Build an encoded share URL and underlying GitHub blob/tree link for a doc or folder.
:::

<ParamField body="kind" type="doc | folder" required>
Target type.
</ParamField>

<ParamField body="docPath" type="string">
Required when `kind` is `doc`. Content-relative path with extension.
</ParamField>

<ParamField body="folderPath" type="string">
Required when `kind` is `folder`. Empty string means content root.
</ParamField>

<ResponseField name="ok" type="boolean">
`true` on success.
</ResponseField>

<ResponseField name="shareUrl" type="string">
`https://openknowledge.ai/d/<encoded>` share link.
</ResponseField>

<ResponseField name="sharedUrl" type="string">
Underlying `github.com/.../blob/...` or `tree/...` URL.
</ResponseField>

<ResponseField name="branch" type="string">
Current git branch used for the link.
</ResponseField>

Error codes (HTTP 200 with `ok: false`): `no-remote`, `detached-head`, `branch-not-on-origin`, `non-github-remote`, `invalid-path`.

<RequestExample>
```bash
curl -s -X POST http://127.0.0.1:7777/api/share/construct-url \
  -H 'Content-Type: application/json' \
  -d '{ "kind": "doc", "docPath": "docs/guide.md" }'
```
</RequestExample>

### Publish flow

| Method | Path | Input | Output |
| --- | --- | --- | --- |
| `GET` | `/api/share/publish/owners` | — | `{ ok, owners: [{ login, kind, avatarUrl? }] }` or `{ ok: false, error }` |
| `GET` | `/api/share/publish/name-check` | `owner`, `name` query params | `{ ok, available }` |
| `POST` | `/api/share/publish` | `{ owner, name, visibility, description? }` | `{ ok, ownerLogin, repoName, cloneUrl, defaultBranch }` or `{ ok: false, error }` |

Publish errors include `name-conflict`, `saml-sso`, `auth-required`, `push-failed`, `init-failed`, `network`, `no-project`. Successful publish triggers a background `syncEngine.refreshRemote()`.

---

## Local operations (`/api/local-op/*`)

Desktop shell routes that spawn `ok` CLI subprocesses or touch secrets on disk. All require loopback connection and permitted origin.

### `POST /api/local-op/ok-init`

:::endpoint POST /api/local-op/ok-init
Initialize `.ok/` scaffolding on an absolute path inside the user home directory.
:::

<ParamField body="projectPath" type="string" required>
Absolute filesystem path to a git working tree.
</ParamField>

<ResponseField name="ok" type="boolean">
`true` when initialized or already initialized.
</ResponseField>

<ResponseField name="projectPath" type="string">
Canonical realpath on success.
</ResponseField>

<ResponseField name="reason" type="not-a-git-worktree | init-failed">
Present when `ok` is `false`.
</ResponseField>

- Non-absolute `projectPath` → `400` (`urn:ok:error:invalid-request`)
- Path outside home → `400` (`urn:ok:error:dir-outside-home`)
- Concurrent init → `429` with `Retry-After: 2`
- Idempotent: re-calling on an initialized project returns `{ ok: true }` without rewriting `config.yml`

<RequestExample>
```bash
curl -s -X POST http://127.0.0.1:7777/api/local-op/ok-init \
  -H 'Content-Type: application/json' \
  -d '{ "projectPath": "/Users/me/projects/my-wiki" }'
```
</RequestExample>

### `POST /api/local-op/clone`

<ParamField body="url" type="string" required>
Git remote URL (`https://`, `ssh://`, `git://`, or SCP-style `git@host:repo`). Blocks `file://`, `javascript:`, etc.
</ParamField>

<ParamField body="dir" type="string" required>
Destination directory within home (`~` expanded).
</ParamField>

<ParamField body="branch" type="string">
Optional branch name (validated).
</ParamField>

Returns `200` with `Content-Type: application/x-ndjson`. Progress events are JSON lines; completion line includes `{ type: "complete", port, dir }` after starting the project server. Timeout: 10 minutes. Concurrent clone → `429` (`Retry-After: 30`).

### Auth and embeddings

| Method | Path | Behavior |
| --- | --- | --- |
| `POST` | `/api/local-op/auth/login` | Device-flow NDJSON stream; optional `{ host }` (default `github.com`) |
| `POST` | `/api/local-op/auth/status` | `{ authenticated: boolean }` |
| `POST` | `/api/local-op/auth/repos` | Lists repos accessible to stored credentials |
| `POST` | `/api/local-op/auth/signout` | Clears stored auth |
| `POST` | `/api/local-op/auth/set-identity` | `{ name, email }` git identity |
| `POST` | `/api/local-op/embeddings/set-key` | `{ key }` → `{ keyPresent: true }` |
| `POST` | `/api/local-op/embeddings/clear-key` | → `{ keyPresent: false }` |

---

## Verify routes locally

<Steps>
<Step title="Start the collab server">
Run `ok start` (or `ok start --open`) in an initialized project. Note the bound loopback port from CLI output or `GET /api/config`.
</Step>
<Step title="Check bootstrap">
```bash
curl -s http://127.0.0.1:<port>/api/config | jq .
```
Confirm `collabUrl` and `port` match expectations.
</Step>
<Step title="Exercise search and listing">
```bash
curl -s 'http://127.0.0.1:<port>/api/search?query=readme&intent=omnibar' | jq '.results[:3]'
curl -s 'http://127.0.0.1:<port>/api/documents' | jq '.documents | length'
```
</Step>
<Step title="Confirm loopback gate">
A mutating request from a non-loopback peer or with `Host: evil.example.com` should return `403` (`urn:ok:error:loopback-required` or `urn:ok:error:host-not-allowed`).
</Step>
</Steps>

<AccordionGroup>
<Accordion title="Common error URNs">
| URN | Typical cause |
| --- | --- |
| `urn:ok:error:invalid-request` | Schema validation failure, bad `docName`, query too long |
| `urn:ok:error:loopback-required` | Remote peer on a mutating or local-op route |
| `urn:ok:error:host-not-allowed` | `Host` header not localhost/127.x |
| `urn:ok:error:invalid-origin` | `Origin` not on loopback |
| `urn:ok:error:doc-not-found` | Document missing on read/patch |
| `urn:ok:error:reserved-doc-name` | Write to system/config doc |
| `urn:ok:error:concurrent-operation` | Another clone/auth/publish/init in flight |
| `urn:ok:error:payload-too-large` | Search POST body &gt; ~1 MiB |
</Accordion>
<Accordion title="MCP vs HTTP writes">
MCP `write` and `edit` tools call these same HTTP routes when a collab server is running. Server-free MCP reads (`exec`, `get_preview_url`) do not use this route table — see the collaboration server lifecycle for the split.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="Collaboration server" href="/collaboration-server">
Hocuspocus lifecycle, per-project locks, and when MCP routes writes through HTTP.
</Card>
<Card title="MCP tools reference" href="/mcp-tools-reference">
Seventeen MCP tools that delegate to these HTTP endpoints for mutations.
</Card>
<Card title="Team sharing" href="/team-sharing">
`ok share` CLI commands backing `/api/share/*` routes.
</Card>
<Card title="Semantic search setup" href="/semantic-search-setup">
Configure embeddings keys via `/api/local-op/embeddings/*` and `search.semantic.*` config.
</Card>
<Card title="Auth reference" href="/auth-reference">
GitHub OAuth device flow and token storage used by `/api/local-op/auth/*`.
</Card>
</CardGroup>
