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)