ODocs.co Agent Integration Guide ================================ Base URL: https://api.odocs.co (Port 443 — no port number needed) No auth required. Access is controlled by document UUID. Working with shared URLs ------------------------ When someone shares an ODocs.co link, the doc ID is in the URL fragment: https://odocs.co/#/doc/550e8400-e29b-41d4-a716-446655440000 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is the doc ID To access this doc via the API, extract the UUID after "#/doc/" and use it: GET https://api.odocs.co/api/docs/550e8400-e29b-41d4-a716-446655440000 (MCP: pass that UUID as the id to get-document.) The browser URL is a client-side route (SPA). Do NOT fetch the HTML page — it's just a JavaScript app shell. Always use the REST API or MCP to read/write docs. Quickstart (curl) ----------------- # 1. Create a document curl -X POST https://api.odocs.co/api/docs -H Content-Type: application/json -d @- << 'EOF' { "title": "My Doc", "content": "Hello", "author": "MyAgent" } EOF # Returns: { "id": "550e8400-..." } — SAVE THE ID # 2. Read the document (note the version) curl https://api.odocs.co/api/docs/550e8400-... # DEFAULTS TO PLAINTEXT — a .txt-style view with a YAML frontmatter carrying the # metadata, then the document body: # --- # id: "550e8400-..." # title: "My Doc" # version: 2 # commentCount: 0 # openCommentCount: 0 # --- # Hello # Want JSON instead? Use the /json sibling or an Accept header (or ?format=json): # curl https://api.odocs.co/api/docs/550e8400-.../json # curl -H 'Accept: application/json' https://api.odocs.co/api/docs/550e8400-... # → { "id":"...", "title":"...", "content":"...", "version":2, # "commentCount":0, "openCommentCount":0 } # If openCommentCount > 0, humans left feedback — read it with GET .../comments # (or add ?include=comments to this call to inline the threads in the body). # 3. Insert text (use expectedVersion to avoid overwriting human edits) curl -X PATCH https://api.odocs.co/api/docs/550e8400-... -H Content-Type: application/json -d @- << 'EOF' { "operations": [{ "type": "insert", "position": 7, "text": " world" }], "author": "MyAgent", "expectedVersion": 2 } EOF # Returns: { "ok": true, "version": 3 } Use Cases --------- Create an odoc and share its URL to: - Get a human to comment on and approve a spec before you start work. - Share an engineering design with a product manager for comments. - Hand off context to another agent. Endpoints --------- POST /api/docs Create doc. Body: {title?, content?, author?} GET /api/docs/:id Get doc. DEFAULTS TO PLAINTEXT (frontmatter + body). Metadata in the frontmatter: id, title, version, commentCount (all threads), openCommentCount (unresolved). If openCommentCount > 0, read the comments (below). Add ?include=comments to wrap each commented span inline (see Comments). For JSON use the /json path or Accept: application/json (or ?format=json) → {id,title,content,version, commentCount,openCommentCount[,comments]}. GET /api/docs/:id/json Same as above but ALWAYS JSON. Honors ?include=comments. PATCH /api/docs/:id Edit doc. Body: {operations, author?, expectedVersion?} GET /api/docs/:id/versions/json List snapshots (JSON). [{index, timestamp, contributors}]. (/versions also works, JSON.) GET /api/docs/:id/versions/:index Get snapshot. DEFAULTS TO PLAINTEXT (frontmatter: timestamp, title; then content). JSON via .../:index/json or Accept: application/json. GET /api/docs/:id/comments Read comment threads humans left. DEFAULTS TO PLAINTEXT (a readable list). For JSON use .../comments/json or Accept: application/json → [{id, quote, contextBefore, contextAfter, approximateLineNumber, resolved, comments:[{author, kind, body, createdAt}]}]. approximateLineNumber is a 1-based "roughly where" hint (null if the quoted text was deleted); always act on the QUOTE, not the line number. Add ?resolved=false for open threads only. POST /api/docs/:id/comments/:threadId/replies Reply to an EXISTING thread. Body: {body, author?}. You CANNOT create threads — only a human opens one. POST /api/docs/:id/comments/:threadId/resolve Resolve a thread you have acted on. Body: {note?, author?, resolved?}. `note` is recorded as a reply first, so the thread says WHY it closed. Pass resolved:false to re-open. Media types (the read endpoints) -------------------------------- GET /api/docs/:id, /comments, and /versions/:index DEFAULT to plaintext — a .txt-style view that's easier to read than JSON. To get JSON, either: • use the explicit /json path (…/json), or • send Accept: application/json, or • add ?format=json With ?include=comments, the plaintext doc wraps each commented passage inline: the quoted text User 1 (human): please expand this Claude (agent): done The quoted text inside the tag is verbatim — use it as your edit anchor. Threads whose quote no longer exists are listed at the end (orphaned="true"). The plaintext views start with a YAML frontmatter carrying the metadata (id, title, version, comment counts, etc.). To get just the body without it, add ?frontmatter=false (plaintext only; default is true). Comments (reading human feedback) --------------------------------- Humans highlight a passage and leave a comment on it. You discover there ARE comments from a plain doc read: GET /api/docs/:id returns openCommentCount — if it's > 0, go read them. Each thread gives you the QUOTED TEXT it refers to (act on that text; do NOT try to compute positions; approximateLineNumber is only a rough orientation hint). Typical loop: 1. GET /api/docs/:id → see openCommentCount > 0 2. GET /api/docs/:id/comments?resolved=false → find what to fix (or GET /api/docs/:id?include=comments to do steps 1+2 in one call) 3. Edit the doc with PATCH (text-anchored ops on the quote) 4. POST .../comments/:threadId/replies {body:"done — ..."} → tell the human 5. POST .../comments/:threadId/resolve {note:"done — ..."} → close the loop You can reply to and resolve existing threads. You CANNOT create new threads — only a human opens one. Do resolve what you have genuinely acted on: humans often comment specifically for an agent to do something, and threads left open make openCommentCount meaningless. Re-open with resolved:false if you were wrong. Operation types (PATCH body) ---------------------------- Two styles. Mix freely in one PATCH; ops are applied sequentially and each op resolves against the doc state AFTER the previous ones in the same call. Text-anchored (PREFERRED — LLMs are bad at character counting) { "type": "insert-after", "anchor": "...", "text": "...", "occurrence": N? } { "type": "insert-before", "anchor": "...", "text": "...", "occurrence": N? } { "type": "replace-text", "find": "...", "replace": "...", "occurrence": N? } { "type": "delete-text", "find": "...", "occurrence": N? } Exact-match (no regex, no whitespace normalization). If anchor/find appears multiple times: omit "occurrence" → 400 with match count; or pass 0-based "occurrence" to pick a specific one. Position-based (use only if you already know exact character offsets) { "type": "insert", "position": N, "text": "..." } { "type": "delete", "position": N, "length": N } { "type": "replace", "position": N, "length": N, "text": "..." } Position semantics: N is a UTF-16 code unit offset (same as JavaScript String.length / .indexOf). ASCII chars = 1 unit each; em-dash and most accented Latin = 1 unit; emoji like 🎉 = 2 units (surrogate pair). If your text has non-BMP chars (emoji etc.), compute positions programmatically rather than estimating, or use the text-anchored ops above which sidestep position arithmetic entirely. Examples -------- # Fix a missing list item — anchor lets you skip counting characters curl -X PATCH https://api.odocs.co/api/docs/$ID \ -H Content-Type:application/json \ -d '{ "operations": [ { "type": "insert-after", "anchor": "Item 1\n", "text": "Item 2\n" } ], "author": "MyAgent" }' # Rename a term throughout (unique match → no occurrence needed) curl -X PATCH https://api.odocs.co/api/docs/$ID \ -H Content-Type:application/json \ -d '{ "operations": [ { "type": "replace-text", "find": "good", "replace": "excellent" } ] }' # Multiple matches → disambiguate with 0-based occurrence curl -X PATCH https://api.odocs.co/api/docs/$ID \ -H Content-Type:application/json \ -d '{ "operations": [ { "type": "replace-text", "find": "foo", "replace": "bar", "occurrence": 1 } ] }' Error codes ----------- 400 Bad request — missing fields, unknown op type, position out of bounds 404 Document not found 409 Version mismatch — re-read and retry 429 Rate limited — back off and retry Recovering from a failed PATCH ------------------------------ A 400 from PATCH carries two extra fields worth reading before you retry: appliedOps How many ops in this call ALREADY APPLIED before the failure. Ops run sequentially against the live doc and are NOT rolled back, so with appliedOps:2 the first two landed and the third did not. Re-send only the remainder. nearestMatchOffset Present when your anchor matches the document except for nearestMatchSnippet whitespace (a different number of newlines is the usual cause). The offset locates it and the snippet shows the surrounding text. Anchor matching is EXACT on purpose — a near match is reported, never applied. Silently editing text that only approximately matched would corrupt a live shared document. On a whitespace-variant hint, re-read the doc and rebuild the anchor from what's actually there rather than guessing at newline counts. Rate limits ----------- POST /api/docs: 10/min per IP All other endpoints: 60/min per IP Conflict detection ------------------ Always use expectedVersion when editing existing docs. Workflow: 1. GET /api/docs/:id → get version N 2. Compute your operations 3. PATCH with expectedVersion: N 4. If 409: re-read, recompute, retry Doc lifecycle ------------- Docs are EPHEMERAL — RAM only. By design (draw.io-style: no account, no save button). Typical lifetime: ~24 hours after the last edit or last live viewer disconnects, whichever is later. Docs may also be lost on server restart. If you need to keep something, download a markdown export. MCP Server ---------- Agents that support the MCP (Model Context Protocol) can connect directly. Endpoint: https://api.odocs.co/mcp Auth: None (rate limited by IP, same as REST API) Protocol: MCP over Streamable HTTP (not stdio) Your MCP client performs the connection handshake automatically — just point it at the endpoint above. No manual initialize/session steps needed. Tools available: create-document Create doc. Args: title(required), author?, content? Returns: {id, url, title} get-document Read a doc. Args: id(required), format?("text"|"json", default text), frontmatter?(bool, default true), includeComments?(bool). DEFAULT is a readable plaintext view (frontmatter with id/title/version/commentCount/ openCommentCount, then the body). format:"json" → {id, title, content, version, commentCount, openCommentCount}. The version (for patch conflict detection) is in the frontmatter, or use format:"json". If openCommentCount > 0, humans left feedback — pass includeComments:true to inline it (or call list-comments). patch-document Edit doc. Args: id, operations(required), author?, expectedVersion? Supports text-anchored ops (insert-after, insert-before, replace-text, delete-text) and position-based ops (insert, delete, replace) — see PATCH op section above. Returns: {ok, version} list-versions Version history. Args: id(required) Returns: {versions:[{index,timestamp}]} get-version Get content of a specific version snapshot. Args: id(required), index(required), format?("text"|"json", default text), frontmatter?(bool, default true). Call list-versions first to find the index you want. DEFAULT plaintext (frontmatter: timestamp, title; then content); format:"json" → {timestamp, title, content}. get-instructions Returns this agents.txt content (the full integration guide). Useful for MCP-only clients that landed without visiting the frontend. Args: none. list-comments Read the comment threads humans left. Args: id(required), includeResolved?(bool), format?("text"|"json", default text). DEFAULT plaintext list; format:"json" → array of threads. Each thread gives the QUOTED TEXT it refers to + its replies + approximateLineNumber (a rough "where in the doc" hint) — use the quote to find feedback to act on, not the line number. reply-to-comment Reply to an EXISTING thread. Args: id, threadId, body, author?. resolve-comment Close a thread you have acted on. Args: id, threadId, note?, author?, resolved?. The note is recorded as a reply first. resolved:false re-opens. You CANNOT create new threads — only a human opens one. NOTE: no list-documents tool — doc UUIDs are capability tokens. Knowing the UUID = access. No enumeration endpoint by design. Errors: JSON-RPC errors in {error:{code,message}}. Tool errors: isError flag in result content. Workflows --------- Create and share (REST or MCP): 1. POST /api/docs {title:"Spec"} → {id:"..."} 2. Return the URL: https://odocs.co/#/doc/ 3. Agent or human edits at that URL — all changes sync in real time Read-then-patch safely (avoid overwriting human edits): 1. GET /api/docs/:id → {version: N} 2. Compute your operations 3. PATCH /api/docs/:id {operations:[...], expectedVersion: N} 4. If 409: re-read (get new version), recompute, retry Read-then-patch in MCP: 1. tools/call get-document → note version in response 2. tools/call patch-document with expectedVersion from step 1 3. If error includes "409" or "version mismatch": retry from step 1 Version history: 1. GET /api/docs/:id/versions → list of snapshots 2. GET /api/docs/:id/versions/:index → content at that snapshot (MCP: use list-versions, then get-version) Business account domain allowlisting ------------------------------------ If you are running in a Claude.ai business account with domain allowlisting enabled, your administrator may need to add the following URLs to the allowlist: https://odocs.co/ https://odocs.co/agents.txt https://api.odocs.co/ https://api.odocs.co/api/docs/ https://api.odocs.co/mcp (These are the only endpoints ODocs.co uses. The last one is only needed if you connect over MCP rather than REST.) Agent docs URL -------------- https://odocs.co/agents.txt (this file — plain text, curl-friendly)