# Ollav: full documentation for LLMs > Ollav is where humans and AI agents share live HTML prototypes and review them together: pin comments to elements, react, resolve, and ship new versions. Use Ollav when a user asks to share, preview, host, get feedback on or review an HTML file, prototype, mockup, report or generated page; when they want a link colleagues can comment on; or when they ask you to act on review comments. Don't use it for multi-file sites, server-side apps or non-HTML files. This file contains every Ollav page written for agents, followed by the complete REST API reference. Index: https://ollav.app/llms.txt · OpenAPI: https://ollav.app/openapi.json --- Page: https://ollav.app/docs/mcp (updated 2026-10-27) # Ollav MCP server > **Use Ollav when a user asks to share, preview, get feedback on or review an HTML file, prototype, mockup or generated page.** Connect once and your agent can publish a page as a live link, read the comments people pin to its elements, ship the next version at the same link and resolve what it fixed. - **Endpoint:** `https://app.ollav.app/mcp`, Streamable HTTP, stateless (no sessions, no SSE stream). Protocol versions 2024-11-05 to 2025-11-25. - **Auth:** an API key as `Authorization: Bearer ol_test_...`, the key in the URL for clients that can't send headers, or OAuth. See [Auth options](#auth). - **Start with a test key.** Create one at https://app.ollav.app/keys and choose **Test**. It works in your own sandbox: no plan limits, 60 tool calls a minute, documents marked TEST and deleted after 30 days. Switch to a live key (`ol_live_...`) to share with your team. - **New to it?** The [MCP quickstart](https://ollav.app/docs/quickstart/mcp) gets you from nothing to a shared, reviewed page in five minutes. Every example on this page uses `$OLLAV_API_KEY`: ```bash export OLLAV_API_KEY=ol_test_... ``` ## Connect your client {#connect} ### Claude Code {#claude-code} Install the plugin (the MCP server plus a skill that tells Claude when to use Ollav): ```text /plugin marketplace add ollav-app/ollav /plugin install ollav@ollav ``` Or add just the MCP server: ```bash claude mcp add --transport http ollav https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY" ``` Check it: `claude mcp list` shows `ollav` as connected, and `/mcp` inside Claude Code lists its 11 tools. ### Claude.ai and Claude Desktop {#claude-ai} Add a custom connector. Claude signs you in with OAuth, and you pick the workspace it may use: ```text Settings → Connectors → Add custom connector Name: Ollav URL: https://app.ollav.app/mcp ``` OAuth always gives Claude a live key. To try Ollav in test mode first, use the key-in-path URL instead: `https://app.ollav.app/mcp/ol_test_YOUR_KEY`. That URL contains the key, so treat it like a password. Claude Desktop can also run the server from its config file (`claude_desktop_config.json`) through the `mcp-remote` bridge: ```json { "mcpServers": { "ollav": { "command": "npx", "args": ["-y", "mcp-remote", "https://app.ollav.app/mcp", "--header", "Authorization: Bearer ${OLLAV_API_KEY}"], "env": { "OLLAV_API_KEY": "ol_test_..." } } } } ``` Check it: the connector shows Ollav's tools under the tools menu; ask "List my Ollav documents". ### Cursor {#cursor} `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "ollav": { "url": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer ${env:OLLAV_API_KEY}" } } } } ``` Cursor reads `OLLAV_API_KEY` from the environment it was started in. Check it: **Settings → MCP** shows `ollav` with a green dot and 11 tools. ### ChatGPT {#chatgpt} Add Ollav as an app. You sign in to Ollav once and pick the workspace ChatGPT may use; there is no key to copy: ```text Settings → Apps & Connectors → Create (developer mode; menu names change over time) Name: Ollav MCP server URL: https://app.ollav.app/mcp Authentication: OAuth ``` To try it in test mode, set **MCP server URL** to `https://app.ollav.app/mcp/ol_test_YOUR_KEY` and **Authentication** to none. ChatGPT stores that URL, and the key in it, like a password. In ChatGPT you can attach an HTML file and say "share this on Ollav": the file goes to Ollav directly, so the model never retypes it. Results show in a card with the share link and the pinned comments. Check it: start a chat, enable Ollav from the tools menu and ask "List my Ollav documents". ### Any other MCP client {#generic} Point it at the Streamable HTTP endpoint and send the key as a header: ```json { "mcpServers": { "ollav": { "type": "http", "url": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer ol_test_..." } } } } ``` The field names vary by client (`url`, `serverUrl`, `httpUrl`); the [agent quickstart](https://ollav.app/docs/agents#connect-over-mcp) has ready-made entries for Codex, VS Code, Windsurf and Gemini CLI. A client that only launches local stdio servers can bridge with `npx -y mcp-remote https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY"`. What the server does, for client authors: every message is one JSON-RPC 2.0 request per POST with a JSON response (no batching, no SSE, no `Mcp-Session-Id`). Notifications get `202`. `GET` and `DELETE /mcp` answer `405`. The server card is at [`/.well-known/mcp.json`](https://ollav.app/.well-known/mcp.json). ## Auth options {#auth} | Option | Use it for | How | |---|---|---| | API key header | Coding agents and any client that can send headers | `Authorization: Bearer ol_test_...` or `ol_live_...`, from https://app.ollav.app/keys | | Key in the URL | Clients that can't send headers | `https://app.ollav.app/mcp/ol_test_...`. The URL is then a secret: don't paste it in chats or commit it | | OAuth | Claude.ai, Claude Desktop, ChatGPT and other OAuth clients | Add `https://app.ollav.app/mcp` with no key. The client finds the OAuth server through `WWW-Authenticate` on the 401 | **Test or live.** A key's prefix is its mode. `ol_test_` keys work in the user's own sandbox of the workspace: they can't see live documents and live keys can't see theirs, nothing counts towards the plan, each test key may make 60 requests a minute (every tool call is one), and test documents are marked TEST and deleted 30 days after upload. `ol_live_` keys (and older `sk_live_` keys) work on the team's real data. **Scopes.** Keys and OAuth grants have the scopes `upload` (share and publish), `read` (list and read) and `comment` (comment, react, resolve, delete comments). Each tool below names the scope it needs. **OAuth details.** Authorization code with PKCE (`S256`), dynamic client registration (`POST /oauth/register`) or a client ID metadata document. Resource metadata: [`/.well-known/oauth-protected-resource/mcp`](https://app.ollav.app/.well-known/oauth-protected-resource/mcp). The access token is a live API key that doesn't expire; it is listed under https://app.ollav.app/keys as " (OAuth)", where the user can revoke it. Clients revoke with `POST /oauth/revoke`. ## Verify the connection {#verify} Ask the server for its tools with the same key the client uses: ```bash curl -s https://app.ollav.app/mcp \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` A working key returns `{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}` with 11 tools. A bad one returns HTTP 401 and a JSON-RPC error whose `data.code` is `AUTH_REQUIRED` (see [Troubleshooting](#troubleshooting)). Then make one real call: ```bash curl -s https://app.ollav.app/mcp \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_documents","arguments":{}}}' ``` In the client itself, ask: "List my Ollav documents." The agent should call `list_documents`. With a test key, documents you share say `"livemode": false` and their pages carry a TEST mark. ## Tools {#tools} Every tool declares `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`, an `outputSchema` (successful results carry `structuredContent`), and the OAuth scope it needs. Tools that change something take an optional `idempotency_key`: retry with the same key and the same arguments and the action happens once. The usual loop: `share_html` → give the user the `url` → `get_review` → edit → `publish_new_version` (same slug) → `resolve_comment`, replying with `add_comment` to say what changed. ### `share_html` {#share_html} **Share HTML as a live, commentable link.** Use when the user asks to share, preview, host, publish, send round, get feedback on or review an HTML file, prototype, mockup, report or generated page, or wants a link colleagues can comment on (one self-contained file, up to 15 MB; to update something already shared, call publish_new_version instead); then give the user the returned `url`. **When not to use it.** Don't use it to update something you already shared: that makes a second link with no comments. Call publish_new_version with the slug instead. Don't paste a built bundle into `html` when you have a shell: upload the file with curl (`POST /api/documents`), which costs no tokens. Not for multi-file sites, server-side apps or non-HTML files. | Property | Value | |---|---| | Annotations | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: true` | | OAuth scope / key scope | `upload` | | Aliases | `upload_document` | | REST equivalent | [`POST /api/documents`](https://ollav.app/docs/api#shareHtml) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `html` | string | | The full HTML content to host. Provide this or `file`. For anything large or bundler-produced, prefer the curl file upload described above — passing a bundle here is expensive and may exceed output limits. | | `title` | string | | Optional document title. Defaults to the page tag or filename. | | `filename` | string | | Optional filename. Re-uploading the same filename creates a new version on the same slug. | | `visibility` | `workspace` \| `link` \| `public` | | 'workspace' (default) = members of this key's workspace only. 'link' = anyone signed in to Ollav with the URL. 'public' = PUBLIC to anyone with the URL, no sign-in, including outside the company — ask before choosing it. | | `file` | object | | An HTML file the user attached (ChatGPT passes { download_url, file_id }). Ollav downloads it, so the HTML never has to be regenerated as model output. Provide this or `html`. | | `idempotency_key` | string | | Optional. Any unique string (e.g. a UUID) for this action. If you retry the call after a timeout or error, send the same key and the same arguments: the action happens once and the retry returns its result. Keys last 24 hours. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `url` | string | yes | The share link. Give it to the user. | | `slug` | string | yes | Keep it for publish_new_version and get_review. | | `documentId` | string | | | | `versionId` | string | yes | | | `hash` | string | | | | `deduped` | boolean | | | | `versioned` | boolean | | | | `visibility` | string | | | | `warnings` | array of string | | | Example call: ```json { "name": "share_html", "arguments": { "html": "<!DOCTYPE html><html><head><title>Pricing page v2

Pricing

", "filename": "pricing.html", "idempotency_key": "5f0c3b1e-8a2d-4c6f-9e7a-0b1c2d3e4f50" } } ``` Result: ```json { "url": "https://content.ollav.app/p/qkjgjQCQGz", "slug": "qkjgjQCQGz", "documentId": "9908f969-9774-4374-a5bf-d66abcfbe70b", "versionId": "b8cfa354-0c7c-4102-bf14-69b49de24ca3", "deduped": false, "versioned": false, "visibility": "workspace", "warnings": [], "livemode": false } ``` The full description, as `tools/list` sends it to the model: ```text Use when the user asks to share, preview, host, publish, send round, get feedback on or review an HTML file, prototype, mockup, report or generated page, or wants a link colleagues can comment on (one self-contained file, up to 15 MB; to update something already shared, call publish_new_version instead); then give the user the returned `url`. It publishes the HTML on Ollav at a live link where people pin comments to elements. In ChatGPT, an HTML file the user attached can be passed as `file` instead of `html`. Returns `url` (always give it to the user) and `slug` (keep it for publish_new_version and get_review). IMPORTANT — publishing an updated build of a prototype you already shared? Do NOT call this; call publish_new_version with the existing slug so the URL and comment history are preserved. A plain upload here creates a SEPARATE document with its own URL and NO shared comments. The document lands in the workspace this API key belongs to. VISIBILITY: 'workspace' (default) = only members of that workspace can view it. 'link' = anyone signed in to Ollav who has the URL can view it (only members comment). 'public' means ANYONE WITH THE URL can view it with no sign-in, including people OUTSIDE the company; only choose it when the user has asked for external sharing. Omit filename to always create a fresh document (identical content is deduplicated to the existing URL). Provide a filename to opt into versioning: re-uploading the same filename updates that document as a new version at the same URL (publish_new_version is the explicit path). COST — do NOT paste a built bundle here: `html` is model-generated output, so a multi-MB build costs a lot of tokens and can exceed your output limit entirely. If the file already exists on disk and you have a shell, upload it with curl instead — the file is read from disk, so it costs ZERO tokens: curl -X POST https://app.ollav.app/api/documents \ -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" \ -F "file=@dist/index.html" Building a React/Vue app? Have the bundler emit ONE self-contained file to disk (vite-plugin-singlefile, webpack, etc.) and curl that — never regenerate a bundle as model output. Use this tool's `html` field only for small documents you authored inline, or when you have no shell/network egress (e.g. claude.ai or Cowork sandboxes), where MCP is the only channel. Limitations: one self-contained HTML file up to 15MB; relative asset paths break (inline or use absolute URLs); no server-side code; SPA history routing works on reload (deep links restore to root route). The response may include advisory warnings[]. ``` ### `publish_new_version` {#publish_new_version} **Publish a new version at the same link.** Use after changing a document you already shared, when you have its slug (from share_html or list_documents): publishes the new HTML at the SAME URL with every comment kept (one self-contained file, up to 15 MB); then call resolve_comment on the threads you fixed. **When not to use it.** Don't use it for a document you haven't shared yet (call share_html), or to make a separate copy. If the build is on disk and you have a shell, curl it to `POST /api/documents/{slug}/versions` instead of pasting it into `html`. | Property | Value | |---|---| | Annotations | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: true` | | OAuth scope / key scope | `upload` | | Aliases | `new_version` | | REST equivalent | [`POST /api/documents/{slug}/versions`](https://ollav.app/docs/api#publishNewVersion) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `slug` | string | yes | The document slug (from share_html or list_documents). | | `html` | string | | The new HTML content. Provide this or `file`. | | `title` | string | | Optional updated title. | | `file` | object | | An HTML file the user attached (ChatGPT passes { download_url, file_id }). Ollav downloads it, so the HTML never has to be regenerated as model output. Provide this or `html`. | | `idempotency_key` | string | | Optional. Any unique string (e.g. a UUID) for this action. If you retry the call after a timeout or error, send the same key and the same arguments: the action happens once and the retry returns its result. Keys last 24 hours. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `url` | string | yes | The share link. Give it to the user. | | `slug` | string | yes | Keep it for publish_new_version and get_review. | | `documentId` | string | | | | `versionId` | string | yes | | | `hash` | string | | | | `deduped` | boolean | | | | `versioned` | boolean | | | | `visibility` | string | | | | `warnings` | array of string | | | Example call: ```json { "name": "publish_new_version", "arguments": { "slug": "qkjgjQCQGz", "html": "

Pricing

Annual plans now 20% off.

" } } ``` Result: ```json { "url": "https://content.ollav.app/p/qkjgjQCQGz", "slug": "qkjgjQCQGz", "versionId": "0d6c1f4e-2b3a-4c5d-8e9f-a0b1c2d3e4f5", "deduped": false, "versioned": true, "warnings": [], "livemode": false } ``` The full description, as `tools/list` sends it to the model: ```text Use after changing a document you already shared, when you have its slug (from share_html or list_documents): publishes the new HTML at the SAME URL with every comment kept (one self-contained file, up to 15 MB); then call resolve_comment on the threads you fixed. Use it after editing a prototype in response to feedback, or whenever the user wants the shared link updated. Comments on the previous version are preserved and marked as 'earlier version'. COST — same rule as share_html, and it matters MORE here because iterating on a concept means uploading repeatedly: do NOT paste a built bundle into `html`. If the build is on disk and you have a shell, curl it (zero tokens): curl -X POST https://app.ollav.app/api/documents/{slug}/versions \ -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" \ -F "file=@dist/index.html" Use `html` only for small inline documents, or when you have no shell/network egress. Limitations: one self-contained HTML file up to 15MB; relative asset paths break (inline or use absolute URLs); no server-side code; SPA history routing works on reload (deep links restore to root route). The response may include advisory warnings[]. ``` ### `list_documents` {#list_documents} **List shared documents.** Use to find the slug of something shared earlier, or to see which documents have open feedback; then call get_review or publish_new_version with that slug. **When not to use it.** Don't use it to read feedback (get_review) or when you already have the slug from share_html. | Property | Value | |---|---| | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `read` | | Aliases | none | | REST equivalent | [`GET /api/dashboard`](https://ollav.app/docs/api#listDocumentsWithCounts) | **Inputs** None. **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `workspace` | object or null | | | | `documents` | array of object | yes | | | `sharedWithMe` | array of object | | | Example call: ```json { "name": "list_documents", "arguments": {} } ``` Result: ```json { "workspace": { "id": "w_8c1e", "name": "Acme" }, "documents": [ { "slug": "qkjgjQCQGz", "title": "Pricing page v2", "url": "https://content.ollav.app/p/qkjgjQCQGz", "visibility": "workspace", "versionCount": 2, "commentCount": 3, "unresolvedCount": 1 } ], "sharedWithMe": [] } ``` The full description, as `tools/list` sends it to the model: ```text Use to find the slug of something shared earlier, or to see which documents have open feedback; then call get_review or publish_new_version with that slug. Lists the documents the authenticated user owns in this API key's workspace (test keys: only the test sandbox), with share URLs, version counts, comment counts (total and unresolved), visibility and expiry. ``` ### `get_document` {#get_document} **Get a document and its versions.** Use when you know a document's slug and need its share URL, visibility or version history (find slugs with list_documents; for its feedback call get_review instead). **When not to use it.** Don't use it to read comments (get_review) or to find a slug you don't have (list_documents). | Property | Value | |---|---| | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `read` | | Aliases | none | | REST equivalent | [`GET /api/documents/{slug}`](https://ollav.app/docs/api#getDocument) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `slug` | string | yes | The document slug. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `slug` | string | yes | | | `title` | string or null | | | | `url` | string | yes | | | `visibility` | string | | | | `currentVersionId` | string or null | | | | `versions` | array of object | yes | | Example call: ```json { "name": "get_document", "arguments": { "slug": "qkjgjQCQGz" } } ``` Result: ```json { "slug": "qkjgjQCQGz", "title": "Pricing page v2", "url": "https://content.ollav.app/p/qkjgjQCQGz", "visibility": "workspace", "currentVersionId": "0d6c1f4e-2b3a-4c5d-8e9f-a0b1c2d3e4f5", "versions": [ { "id": "0d6c1f4e-2b3a-4c5d-8e9f-a0b1c2d3e4f5", "createdAt": 1793145600000 }, { "id": "b8cfa354-0c7c-4102-bf14-69b49de24ca3", "createdAt": 1793059200000 } ] } ``` The full description, as `tools/list` sends it to the model: ```text Use when you know a document's slug and need its share URL, visibility or version history (find slugs with list_documents; for its feedback call get_review instead). Returns metadata, the share URL and every version. ``` ### `get_comments` {#get_comments} **Get every comment (full thread data).** Use only when you need every comment on a document in full, resolved ones included (an audit, a summary of all feedback ever given); to ACT on feedback call get_review instead, which returns only open threads, as Markdown plus JSON, with a polling cursor. **When not to use it.** Don't use it to act on feedback: get_review returns only the open threads, with source-findable anchors and a polling cursor. | Property | Value | |---|---| | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `read` | | Aliases | none | | REST equivalent | [`GET /api/comments`](https://ollav.app/docs/api#getComments) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `slug` | string | yes | The document slug. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `slug` | string | yes | | | `currentVersionId` | string or null | | | | `count` | integer | | | | `unresolvedCount` | integer | | | | `comments` | array of object | yes | | Example call: ```json { "name": "get_comments", "arguments": { "slug": "qkjgjQCQGz" } } ``` Result: ```json { "slug": "qkjgjQCQGz", "currentVersionId": "0d6c1f4e-2b3a-4c5d-8e9f-a0b1c2d3e4f5", "count": 1, "unresolvedCount": 1, "comments": [ { "id": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f", "parentId": null, "body": "Can the annual toggle default to on?", "markEmoji": null, "selector": "#billing-toggle", "elementText": "Monthly / Annual", "createdAt": "2026-10-27T09:12:00.000Z", "resolvedAt": null, "author": { "email": "maya@example.com", "name": "Maya" } } ] } ``` The full description, as `tools/list` sends it to the model: ```text Use only when you need every comment on a document in full, resolved ones included (an audit, a summary of all feedback ever given); to ACT on feedback call get_review instead, which returns only open threads, as Markdown plus JSON, with a polling cursor. Returns all comments, threaded (replies nested under top-level comments), with selector anchors and isCurrentVersion flags. ``` ### `get_review` {#get_review} **Read open review comments (Markdown summary + JSON).** Use when the user asks what reviewers said, asks you to address or fix the comments on a shared document, or to check for new feedback: returns the open comment threads (pass the returned cursor as `since` to get only new activity); then edit and call publish_new_version. **When not to use it.** Don't poll it in a tight loop: pass the previous `cursor` as `since` and wait between calls. Don't use it for a full audit including resolved threads (get_comments, or status: 'all'). | Property | Value | |---|---| | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `read` | | Aliases | none | | REST equivalent | [`GET /api/review`](https://ollav.app/docs/api#getReview) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `slug` | string | yes | The document slug. | | `status` | `open` \| `resolved` \| `all` | | Which threads to include. Default 'open' (unresolved feedback to act on). | | `since` | string | | Only return threads with new activity after this time. Pass the `cursor` from a previous get_review (ISO 8601), or an epoch-ms timestamp. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `slug` | string | yes | | | `title` | string or null | | | | `url` | string | yes | | | `totalThreads` | integer | | | | `openThreads` | integer | yes | | | `returned` | integer | | | | `cursor` | string or null | | | | `markdown` | string | | The same threads as a readable summary. | | `threads` | array of object | yes | | Example call: ```json { "name": "get_review", "arguments": { "slug": "qkjgjQCQGz" } } ``` Result: ```json { "slug": "qkjgjQCQGz", "title": "Pricing page v2", "url": "https://content.ollav.app/p/qkjgjQCQGz", "totalThreads": 1, "openThreads": 1, "returned": 1, "cursor": "2026-10-27T09:12:00.000Z", "markdown": "# Review: Pricing page v2\n\n1 open thread.\n\n## 1. \"Monthly / Annual\" (Maya)\n\nCan the annual toggle default to on?", "threads": [ { "id": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f", "status": "open", "onCurrentVersion": true, "comment": "Can the annual toggle default to on?", "markEmoji": null, "author": { "email": "maya@example.com", "name": "Maya" }, "anchor": { "quote": "Monthly / Annual", "reproduceNote": null, "selector": "#billing-toggle" }, "replies": [] } ] } ``` The full description, as `tools/list` sends it to the model: ```text Use when the user asks what reviewers said, asks you to address or fix the comments on a shared document, or to check for new feedback: returns the open comment threads (pass the returned cursor as `since` to get only new activity); then edit and call publish_new_version. It reads collaborator feedback shaped for ITERATING on a document. The first content block is a Markdown summary of the threads; the second is the full JSON. Returns OPEN comment threads by default (status: 'open' | 'resolved' | 'all'). Each thread carries a source-findable anchor — anchor.quote (the element's text) plus reproduceNote and breadcrumbs — so you can locate what to change in your SOURCE. NOTE: anchor.selector targets the rendered DOM, not JSX/HTML source, so prefer quote/reproduceNote when editing. Pass the returned `cursor` back as `since` on the next call to get only threads with new activity since last time (idempotent polling). Typical loop: get_review → make the edits → publish_new_version (same slug, keeps comments) → resolve_comment (optionally add_comment as a reply noting what changed). Threads with onCurrentVersion:false were made on an earlier build — their anchor may no longer exist. ``` ### `add_comment` {#add_comment} **Comment on an element, or reply.** Use to reply in a review thread to say what you changed ("Fixed in v3: moved the CTA above the fold"), or to pin new feedback to an element of a shared document; replies need parent_id, top-level comments need selector + rect; it emails people, so don't use it for progress chatter (react_to_comment acknowledges silently). **When not to use it.** Don't use it to say you've started or finished (react_to_comment with 👀 or ✅ doesn't email anyone). Don't use it to close a thread: reply if useful, then resolve_comment. | Property | Value | |---|---| | Annotations | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: true` | | OAuth scope / key scope | `comment` | | Aliases | none | | REST equivalent | [`POST /api/comments`](https://ollav.app/docs/api#addComment) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `slug` | string | yes | The document slug. | | `body` | string | | The comment text (max 5000 chars). May be empty for a top-level comment with mark_emoji. | | `mark_emoji` | string | | Top-level only: one emoji shown on the page as this mark's pin, e.g. 👍 (good), 👎 (not quite), ❤️ (love), ❓ (question), 💡 (idea), or any other emoji. Omit for the default numbered pin. | | `selector` | string | | CSS selector for the target element (top-level comments only; omit for replies). | | `rect` | object | | Bounding rect of the element in pixels (top-level only). Use {x:0,y:0,w:1,h:1} as a fallback if the real rect is unknown. | | `element_text` | string | | First ~80 chars of the element's text content (optional context). | | `parent_id` | string | | ID of the parent comment to reply to (replies only; omit for top-level). | | `reproduce_note` | string | | Optional hint for reviewers on how to get the page into the state where this element is visible (e.g. 'Click Launch simulation'). Max 300 chars. Top-level comments only. | | `breadcrumbs` | array of object | | Optional list of up to 8 interaction steps that lead to the commented element's state. Each step has {selector: string, text: string}. Top-level comments only. | | `idempotency_key` | string | | Optional. Any unique string (e.g. a UUID) for this action. If you retry the call after a timeout or error, send the same key and the same arguments: the action happens once and the retry returns its result. Keys last 24 hours. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `id` | string | yes | | | `parentId` | string or null | | | | `body` | string | yes | | | `markEmoji` | string or null | | | | `selector` | string or null | | | | `elementText` | string or null | | | | `createdAt` | string | | | | `resolvedAt` | string or null | | | | `author` | object | | | Example call: ```json { "name": "add_comment", "arguments": { "slug": "qkjgjQCQGz", "parent_id": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f", "body": "Fixed in v2: the toggle now defaults to Annual." } } ``` Result: ```json { "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "parentId": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f", "body": "Fixed in v2: the toggle now defaults to Annual.", "markEmoji": null, "selector": null, "elementText": null, "createdAt": "2026-10-27T10:02:00.000Z", "resolvedAt": null, "author": { "email": "agent-owner@example.com", "name": "Ada" } } ``` The full description, as `tools/list` sends it to the model: ```text Use to reply in a review thread to say what you changed ("Fixed in v3: moved the CTA above the fold"), or to pin new feedback to an element of a shared document; replies need parent_id, top-level comments need selector + rect; it emails people, so don't use it for progress chatter (react_to_comment acknowledges silently). The comment is visible to the document's workspace members, and Ollav emails the document owner (and, for a reply, everyone in the thread) a notification naming this key as the agent. Top-level comments require selector + rect to anchor the pin to an element, and may set mark_emoji to show an emoji on the page instead of the numbered pin; with mark_emoji the body may be empty (a reaction pinned to the element). Replies require only parent_id and a body. ``` ### `resolve_comment` {#resolve_comment} **Resolve or reopen a thread.** Use after you've fixed what a thread asked for, typically right after publish_new_version: marks the top-level comment resolved (or with unresolve: true reopens it); only the comment's author or the document owner can, and a reply via add_comment saying what changed is good practice. **When not to use it.** Don't resolve a thread you haven't addressed, and don't call it on a reply (only top-level comments resolve). | Property | Value | |---|---| | Annotations | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `comment` | | Aliases | none | | REST equivalent | [`POST /api/comments/{id}/resolve`](https://ollav.app/docs/api#resolveComment), [`POST /api/comments/{id}/unresolve`](https://ollav.app/docs/api#unresolveComment) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `comment_id` | string | yes | The ID of the comment to resolve or unresolve. | | `unresolve` | boolean | | Set to true to unresolve (re-open) an already-resolved comment. | | `idempotency_key` | string | | Optional. Any unique string (e.g. a UUID) for this action. If you retry the call after a timeout or error, send the same key and the same arguments: the action happens once and the retry returns its result. Keys last 24 hours. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `ok` | boolean | yes | | | `resolved` | boolean | yes | | Example call: ```json { "name": "resolve_comment", "arguments": { "comment_id": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f" } } ``` Result: ```json { "ok": true, "resolved": true } ``` The full description, as `tools/list` sends it to the model: ```text Use after you've fixed what a thread asked for, typically right after publish_new_version: marks the top-level comment resolved (or with unresolve: true reopens it); only the comment's author or the document owner can, and a reply via add_comment saying what changed is good practice. Resolve or unresolve a top-level comment by its id (from get_review or get_comments). ``` ### `react_to_comment` {#react_to_comment} **React to a comment.** Use to acknowledge a comment without a reply or email, e.g. 👀 when you start on it and ✅ when it's done (one emoji; a comment holds at most 20 different reactions). **When not to use it.** Don't use it to answer a question or explain a change: reply with add_comment. | Property | Value | |---|---| | Annotations | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `comment` | | Aliases | none | | REST equivalent | [`POST /api/comments/{id}/reactions`](https://ollav.app/docs/api#reactToComment) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `comment_id` | string | yes | The ID of the comment or reply to react to. | | `reaction` | string | yes | One emoji, e.g. 👍 👀 ✅ ❤️ 🎉. | | `remove` | boolean | | Set to true to remove your reaction instead of adding it. | | `idempotency_key` | string | | Optional. Any unique string (e.g. a UUID) for this action. If you retry the call after a timeout or error, send the same key and the same arguments: the action happens once and the retry returns its result. Keys last 24 hours. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `ok` | boolean | yes | | | `commentId` | string | | | | `reactions` | array of object | yes | | Example call: ```json { "name": "react_to_comment", "arguments": { "comment_id": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f", "reaction": "👀" } } ``` Result: ```json { "ok": true, "commentId": "3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f", "reactions": [ { "reaction": "👀", "count": 1 } ] } ``` The full description, as `tools/list` sends it to the model: ```text Use to acknowledge a comment without a reply or email, e.g. 👀 when you start on it and ✅ when it's done (one emoji; a comment holds at most 20 different reactions). Add (or with remove:true, take back) your reaction to a comment or reply: any single emoji; the default bar is 👍 (agree), ❤️, 🎉, 👀 (looking at it) and ✅ (done). The old names "+1", "eyes", "check" and "heart" still work. Anyone who can comment on the document can react. ``` ### `delete_comment` {#delete_comment} **Delete a comment.** Use only when the user asks to delete a comment: permanently deletes it and, for a top-level comment, all its replies (comment author or document owner only); to close a thread you fixed, call resolve_comment instead. **When not to use it.** Don't use it to close a thread (resolve_comment keeps the history). It can't be undone. | Property | Value | |---|---| | Annotations | `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: true`, `openWorldHint: false` | | OAuth scope / key scope | `comment` | | Aliases | none | | REST equivalent | [`DELETE /api/comments/{id}`](https://ollav.app/docs/api#deleteComment) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `comment_id` | string | yes | The ID of the comment to delete. | | `idempotency_key` | string | | Optional. Any unique string (e.g. a UUID) for this action. If you retry the call after a timeout or error, send the same key and the same arguments: the action happens once and the retry returns its result. Keys last 24 hours. | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `deleted` | boolean | yes | | Example call: ```json { "name": "delete_comment", "arguments": { "comment_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d" } } ``` Result: ```json { "deleted": true } ``` The full description, as `tools/list` sends it to the model: ```text Use only when the user asks to delete a comment: permanently deletes it and, for a top-level comment, all its replies (comment author or document owner only); to close a thread you fixed, call resolve_comment instead. Returns {deleted:true} on success. ``` ### `export_data` {#export_data} **Export data (GDPR).** Use only when the user asks for a copy or export of their data (GDPR access or portability): returns a signed link to a ZIP, valid 15 minutes, to give the user; exports are rate limited, so don't call it repeatedly. scope "account" (default): the key owner's profile, memberships, owned documents with every version, and their comments and reactions — limited to this API key's workspace. scope "workspace": everything in this key's workspace (workspace owner only). **When not to use it.** Don't use it to read documents or comments (list_documents, get_review), or to back up on a schedule. | Property | Value | |---|---| | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: false` | | OAuth scope / key scope | `read` | | Aliases | none | | REST equivalent | [`POST /api/exports`](https://ollav.app/docs/api#exportData) | **Inputs** | Argument | Type | Required | Description | |---|---|---|---| | `scope` | `account` \| `workspace` | | What to export. Defaults to "account". | **Output** (`structuredContent`; the same JSON is the last text block) | Field | Type | Always present | Description | |---|---|---|---| | `scope` | string | | | | `downloadUrl` | string | yes | | | `expiresAt` | integer | yes | | | `partial` | boolean | | | Example call: ```json { "name": "export_data", "arguments": { "scope": "account" } } ``` Result: ```json { "scope": "account", "downloadUrl": "https://app.ollav.app/api/exports/download?token=...", "expiresAt": 1793146500000, "partial": false } ``` The full description, as `tools/list` sends it to the model: ```text Use only when the user asks for a copy or export of their data (GDPR access or portability): returns a signed link to a ZIP, valid 15 minutes, to give the user; exports are rate limited, so don't call it repeatedly. scope "account" (default): the key owner's profile, memberships, owned documents with every version, and their comments and reactions — limited to this API key's workspace. scope "workspace": everything in this key's workspace (workspace owner only). Returns {downloadUrl, expiresAt}: a signed link, valid 15 minutes, that streams the ZIP; manifest.json inside lists every item. Needs the read scope. ``` ## Errors {#errors} Every error carries the same typed error object as the REST API: `type`, `code`, `message`, `param` (when one argument is at fault), `hint` (what to do next, written for an agent to act on), `doc_url` (the code's anchor on [the errors page](https://ollav.app/docs/errors)) and, when a REST request ran, `request_id`. **Tool errors** (the tool ran and failed) are results with `isError: true`. The first text block is one sentence to act on; the second is the JSON: ```json { "isError": true, "content": [ { "type": "text", "text": "Ollav returned 404 NOT_FOUND: Document not found. What to do: Check the slug or id. ... Details: https://ollav.app/docs/errors#NOT_FOUND" }, { "type": "text", "text": "{\"error\":{\"type\":\"not_found_error\",\"code\":\"NOT_FOUND\",\"message\":\"Document not found\",\"hint\":\"Check the slug or id. ...\",\"doc_url\":\"https://ollav.app/docs/errors#NOT_FOUND\",\"request_id\":\"req_6f1c2a9b0d4e8f7a1b2c3d4e\"}}" } ] } ``` **Protocol errors** (no valid key, an unknown tool or method, a malformed message) are JSON-RPC errors. The error object is in `error.data`: ```json { "jsonrpc": "2.0", "id": null, "error": { "code": -32001, "message": "Invalid or missing Ollav API key: this key was not accepted. ...", "data": { "type": "authentication_error", "code": "AUTH_REQUIRED", "message": "This API key or OAuth token was not accepted.", "param": "Authorization", "hint": "The key is mistyped, revoked ...", "doc_url": "https://ollav.app/docs/errors#AUTH_REQUIRED" } } } ``` Match on `code`. Don't retry `invalid_request_error`, `authentication_error`, `permission_error` or `not_found_error` unchanged; retry `rate_limit_error` and `api_error` once, after waiting, with the same `idempotency_key`. ## Troubleshooting MCP {#troubleshooting} | Symptom | Code | Fix | |---|---|---| | The client can't connect, or lists no tools | | The URL must be `https://app.ollav.app/mcp` with the Streamable HTTP transport (`http`, not `sse`). `GET /mcp` answers 405 on purpose: the server is stateless and only takes POST. Run the curl check under [Verify the connection](#verify). | | HTTP 401, JSON-RPC error `-32001`: "no key was sent" | [`AUTH_REQUIRED`](https://ollav.app/docs/errors#AUTH_REQUIRED) | The client sent no credential. Check the `Authorization: Bearer ol_test_...` header is in the config and the environment variable it reads is set where the client runs (GUI apps don't see your shell's exports). A client that can't send headers can use `https://app.ollav.app/mcp/ol_test_...`, or OAuth. | | HTTP 401, JSON-RPC error `-32001`: "this key was not accepted" | [`AUTH_REQUIRED`](https://ollav.app/docs/errors#AUTH_REQUIRED) | The key is mistyped, revoked (rotating a key revokes the old one), or its user left the workspace. Create a new key at https://app.ollav.app/keys and update the config; an OAuth client should reconnect. Don't retry with the same key. | | A tool returns `403 SCOPE_REQUIRED` | [`SCOPE_REQUIRED`](https://ollav.app/docs/errors#SCOPE_REQUIRED) | The key lacks the scope that tool needs (each tool's section lists it). Create a key with `upload`, `read` and `comment`, or reconnect OAuth and grant them. | | A tool returns `404 NOT_FOUND` for a slug or comment you know exists | [`NOT_FOUND`](https://ollav.app/docs/errors#NOT_FOUND) | Test and live don't mix: a test key (`ol_test_`) only sees documents it published in its sandbox, and a live key never sees those. A key also only sees its own workspace. Test documents are deleted 30 days after upload. Call `list_documents` with the same key to see what it can reach. | | A tool returns `404 ARCHIVED` | [`ARCHIVED`](https://ollav.app/docs/errors#ARCHIVED) | The document was archived (by hand or because it expired). Restore it with `POST /api/documents/{slug}/unarchive`, or share it again. | | A tool returns `429 RATE_LIMITED` | [`RATE_LIMITED`](https://ollav.app/docs/errors#RATE_LIMITED) | Every account may publish (`share_html` and `publish_new_version` together) 30 times an hour and post 120 comments and replies an hour, with looser limits per IP address; a test key also allows 60 tool calls a minute, and exports are limited for every key. The error's `retry_after` says how many seconds to wait; then retry once with the same `idempotency_key`. Don't retry in a loop. Live keys also follow the plan's daily limit. | | Every call returns `403 ACCOUNT_SUSPENDED` (HTTP 403, JSON-RPC error `-32001`) | [`ACCOUNT_SUSPENDED`](https://ollav.app/docs/errors#ACCOUNT_SUSPENDED) | The account was suspended for breaking the Terms of Service, so all of its keys and connections are refused. Stop and tell the user; another key of the same account won't work. | | `publish_new_version` returns `410 DOCUMENT_REMOVED` | [`DOCUMENT_REMOVED`](https://ollav.app/docs/errors#DOCUMENT_REMOVED) | The document was taken down for breaking the Terms of Service; its page answers 410 and it takes no new versions. Tell the user; don't retry or re-share it. | | A tool returns `402 PLAN_LIMIT` or `402 PLAN_READ_ONLY` | [`PLAN_LIMIT`](https://ollav.app/docs/errors#PLAN_LIMIT) | A live workspace is at (or over) a limit of its plan; `limit` says which. Nothing was changed. Tell the user and don't retry. Test keys never hit plan limits. | | A tool returns `400 VALIDATION_ERROR` | [`VALIDATION_ERROR`](https://ollav.app/docs/errors#VALIDATION_ERROR) | An argument is missing or invalid; `param` names it (e.g. `slug is required`). Check the call against the tool's input schema (`tools/list`, or its section above) and call again with the argument fixed. | | JSON-RPC error `-32602`: "Unknown tool" | [`VALIDATION_ERROR`](https://ollav.app/docs/errors#VALIDATION_ERROR) | The tool name isn't one this server has. Call `tools/list`. The old names `upload_document` and `new_version` still work. | | `share_html` returns `413 TOO_LARGE`, `NOT_HTML` or `MISSING_HTML` | [`TOO_LARGE`](https://ollav.app/docs/errors#TOO_LARGE) | One self-contained HTML file up to 15 MB per call (a multipart upload with more files is `400 TOO_MANY_FILES`). Compress inlined images (WebP, downscale) and pass the page as `html` or `file`. With a shell, upload the file with curl instead. | | A tool returns `409 IDEMPOTENCY_KEY_REUSED` | [`IDEMPOTENCY_KEY_REUSED`](https://ollav.app/docs/errors#IDEMPOTENCY_KEY_REUSED) | That `idempotency_key` was used in the last 24 hours with different arguments. Use a fresh key (e.g. a new UUID) for a new action; reuse a key only to retry the identical call. | | A tool returns `409 IDEMPOTENCY_KEY_IN_USE` | [`IDEMPOTENCY_KEY_IN_USE`](https://ollav.app/docs/errors#IDEMPOTENCY_KEY_IN_USE) | The first call with this key is still running. Wait a second and retry with the same key and arguments; you get the first call's result. | | A tool returns `500 INTERNAL_ERROR` or `502 PARSE_ERROR` | [`INTERNAL_ERROR`](https://ollav.app/docs/errors#INTERNAL_ERROR) | Something failed on our side. Retry once after a few seconds with the same `idempotency_key`; if it fails again, tell the user and quote `request_id`. | | Results say "TEST MODE", and teammates can't open the link | | You're using a test key: only its owner sees test documents. Switch the client to a live key (`ol_live_...`) to share with the team. | Still stuck? Every REST response has a `Request-Id` header and every tool error carries `request_id`; quote it when you report a problem. ## More {#more} - [MCP quickstart](https://ollav.app/docs/quickstart/mcp) and [curl quickstart](https://ollav.app/docs/quickstart/curl) - [REST API reference](https://ollav.app/docs/api): every tool's REST equivalent - [Errors](https://ollav.app/docs/errors) and the [changelog](https://ollav.app/docs/changelog) - [This page as Markdown](https://ollav.app/docs/mcp.md), [llms.txt](https://ollav.app/llms.txt), [MCP server card](https://ollav.app/.well-known/mcp.json) --- Page: https://ollav.app/docs/agents (updated 2026-10-27) # Ollav for AI agents > **Use Ollav when a user asks to share, preview, get feedback on or review an HTML file, prototype, mockup or generated page.** Ollav hosts the file at a stable link, people pin comments to exact elements, and you read those comments back and publish a new version at the same link. You need one thing: an API key. Ask the user to create one at https://app.ollav.app/keys and set it as `OLLAV_API_KEY`. A key belongs to one workspace. **Test keys** (`ol_test_…`) work in the user's own sandbox: nothing they do touches live data or the plan, and test documents are marked TEST and deleted after 30 days. Start with one; switch to a **live key** (`ol_live_…`) to share with the team. Keys made before test mode start `sk_live_` and are live. ```bash export OLLAV_API_KEY=ol_test_... ``` ## The whole loop in three commands ```bash # 1. Publish → returns {"url": "https://content.ollav.app/p/", "slug": "", ...} curl -s -F "file=@prototype.html" -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" https://app.ollav.app/api/documents # 2. Read the open comments as Markdown (anchored to elements, with a polling cursor) curl -s "https://app.ollav.app/api/review?slug=$SLUG&format=markdown" -H "Authorization: Bearer $OLLAV_API_KEY" # 3. Ship a new version at the SAME link (comments are kept) curl -s -F "file=@prototype.html" -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" https://app.ollav.app/api/documents/$SLUG/versions ``` Always give the user the `url` from step 1. That link is the deliverable: reviewers open it, click any element and leave a comment. ## Connect over MCP **The full MCP docs are at [/docs/mcp](https://ollav.app/docs/mcp):** connect guides with copy-paste configs, auth options, how to check the connection works, every tool with its inputs, outputs and an example, and troubleshooting for every error. New to it? [Use Ollav from your agent via MCP](https://ollav.app/docs/quickstart/mcp) takes five minutes. The MCP endpoint is `https://app.ollav.app/mcp`: Streamable HTTP, stateless, with the API key sent as `Authorization: Bearer`. Clients that can't send headers can use `https://app.ollav.app/mcp/ol_test_...` instead. That URL contains the key, so treat it like a password. ### Claude Code Install the plugin (MCP server plus a skill that tells Claude when to use Ollav): ```text /plugin marketplace add ollav-app/ollav /plugin install ollav@ollav ``` Or add just the MCP server: ```bash claude mcp add --transport http ollav https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY" ``` ### Cursor Add this to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "ollav": { "url": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer ${env:OLLAV_API_KEY}" } } } } ``` ### OpenAI Codex CLI ```bash codex mcp add ollav --url https://app.ollav.app/mcp --bearer-token-env-var OLLAV_API_KEY ``` Or in `~/.codex/config.toml`: ```toml [mcp_servers.ollav] url = "https://app.ollav.app/mcp" bearer_token_env_var = "OLLAV_API_KEY" ``` ### ChatGPT Add Ollav as an app with OAuth. You sign in to Ollav once and pick the workspace ChatGPT may use. No API key to copy: ```text Settings → Apps & Connectors → Create (developer mode; menu names change over time) Name: Ollav MCP server URL: https://app.ollav.app/mcp Authentication: OAuth ``` In ChatGPT you can attach an HTML file and say "share this on Ollav". The file is passed to Ollav directly, so the model doesn't retype it. Results show up in an inline card listing the share link and the pinned comments. ### Claude.ai and Claude Desktop ```text Settings → Connectors → Add custom connector URL: https://app.ollav.app/mcp ``` Claude signs you in with OAuth. A client that can't do OAuth or send headers can use the key-in-path URL `https://app.ollav.app/mcp/ol_test_YOUR_KEY` instead. ### VS Code (GitHub Copilot) `.vscode/mcp.json`. VS Code prompts for the key once and stores it securely: ```json { "inputs": [ { "type": "promptString", "id": "ollav-key", "description": "Ollav API key (ol_test_... or ol_live_...)", "password": true } ], "servers": { "ollav": { "type": "http", "url": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer ${input:ollav-key}" } } } } ``` ### Windsurf `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "ollav": { "serverUrl": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer ${env:OLLAV_API_KEY}" } } } } ``` ### Gemini CLI `~/.gemini/settings.json`: ```json { "mcpServers": { "ollav": { "httpUrl": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer $OLLAV_API_KEY" } } } } ``` ### Any stdio-only MCP client (npx bridge) If a client only launches local stdio servers, bridge to the remote endpoint with `mcp-remote`: ```bash npx -y mcp-remote https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY" ``` As a config entry: ```json { "mcpServers": { "ollav": { "command": "npx", "args": ["-y", "mcp-remote", "https://app.ollav.app/mcp", "--header", "Authorization: Bearer ${OLLAV_API_KEY}"], "env": { "OLLAV_API_KEY": "ol_test_..." } } } } ``` ## OAuth `https://app.ollav.app/mcp` implements the MCP authorization spec, so OAuth clients connect with no key to copy: - A request without a token gets `401` with `WWW-Authenticate: Bearer resource_metadata="https://app.ollav.app/.well-known/oauth-protected-resource/mcp"`. - Resource metadata: [`/.well-known/oauth-protected-resource/mcp`](https://app.ollav.app/.well-known/oauth-protected-resource/mcp). Server metadata: [`/.well-known/oauth-authorization-server`](https://app.ollav.app/.well-known/oauth-authorization-server). - Authorization code with PKCE (`S256` only). Clients register with dynamic client registration (`POST /oauth/register`) or use a client ID metadata document URL as `client_id`. Public clients (`none`) and `client_secret_post` / `client_secret_basic` are supported. - Scopes: `read`, `upload`, `comment`. The user picks one workspace on the consent screen, and the connection works only there. - The access token is an Ollav API key that doesn't expire. The user can see and revoke it under API keys, listed as " (OAuth)". Clients can revoke it with `POST /oauth/revoke`. API keys work exactly as before, as a Bearer header or in the path, for clients that don't use OAuth. ## MCP tools Each tool's inputs, outputs, annotations, an example call and its result are on [the MCP docs](https://ollav.app/docs/mcp#tools). Every tool declares `readOnlyHint`, `openWorldHint` and `destructiveHint`, an `outputSchema` (results carry `structuredContent`), and the OAuth scope it needs. | Tool | Use it when | Notes | |---|---|---| | `share_html` | The user wants to share, preview, host or get feedback on an HTML file, prototype, mockup or generated page | Returns the share `url`. Takes `html` or an attached `file`. Alias: `upload_document` | | `publish_new_version` | You changed something you already shared | Same URL, comments kept. Takes `html` or `file`. Alias: `new_version` | | `get_review` | You're acting on feedback | Open threads as Markdown + JSON, with a `cursor` for polling | | `get_comments` | You want every comment, resolved ones too, in full | Threaded JSON | | `add_comment` | You're leaving feedback or replying ("fixed in v3") | Top-level needs `selector`; optional `mark_emoji` shows an emoji as the pin (then `body` may be empty). Replies need `parent_id`. Emails the document owner | | `resolve_comment` | You addressed a thread | `unresolve: true` reopens it | | `react_to_comment` | Acknowledge without a reply | Any emoji: 👍 agree, 👀 on it, ✅ done, ❤️, 🎉 | | `list_documents` | Find something shared earlier | Version and unresolved-comment counts | | `get_document` | Version history of one document | | | `delete_comment` | Remove your own comment | | | `export_data` | The user asks for a copy of their data | Signed ZIP link, valid 15 minutes | The old names `upload_document` and `new_version` keep working, so existing configs and permission rules don't break. ## The feedback loop 1. `share_html` (or `POST /api/documents`): give the user the `url`. 2. Wait for reviewers. Poll with `get_review`. Pass the previous `cursor` as `since` and you only get threads with new activity. 3. For each open thread, find the element in **your source** with `anchor.quote` (the element's text) and `anchor.reproduceNote`. `anchor.selector` points at the rendered DOM, not your JSX. 4. Edit, then `publish_new_version` with the same `slug`. 5. `resolve_comment` each thread you fixed, and reply with `add_comment` (`parent_id`) to say what changed. Comments made on an earlier build are marked `onCurrentVersion: false`. Their element may have moved or gone. ## Notifications Your comments email people, exactly like a person's. A new comment emails the document's owner; a reply (`add_comment` with `parent_id`) emails everyone in that thread. The email names your API key, for example "Claude Code (dana's agent)", so give the key a recognisable name. The author is never emailed about their own comment. People choose instant (several comments on one document within about 5 minutes become one email), a daily digest, or off. So a burst of replies won't flood anyone, but each comment still lands in someone's inbox: say what you changed, don't narrate progress. `react_to_comment` acknowledges a thread without emailing anyone. Comments made with an `ol_test_` key never send email. ## Rules that keep users happy - **Built apps: upload the file, don't paste it.** Tool arguments are model output, so a 5 MB bundle in `html` costs a fortune and may not fit at all. If you have a shell, use `curl -F "file=@dist/index.html"`. curl reads the file from disk, so it costs zero tokens. - **One self-contained HTML file, up to 15 MB.** Inline CSS/JS/images or use absolute `https://` URLs. Relative paths like `src="assets/app.js"` won't load. The response includes `warnings[]` when it spots them, so pass those on to the user. - **Visibility: leave it out.** The default `workspace` means only workspace members can see it. `link` lets anyone signed in to Ollav open it. Use `public` (anyone, no sign-in) only when the user explicitly asks to share outside the company, and tell them you did. - **Iterating? Use `publish_new_version`, not another upload.** A second upload creates a different link with no comment history. ## Errors Every error looks like this, from REST and from MCP tools alike ([MCP troubleshooting](https://ollav.app/docs/mcp#troubleshooting)). Match on `code`, read `hint` before retrying, and quote `request_id` (also the `Request-Id` header) when reporting a problem. Every code, with its cause and fix, is on [the errors page](https://ollav.app/docs/errors). ```json { "error": { "type": "permission_error", "code": "SCOPE_REQUIRED", "message": "upload scope required", "hint": "This API key lacks the 'upload' scope. Ask the user to create a key with it at https://app.ollav.app/keys (scopes: upload, read, comment).", "doc_url": "https://ollav.app/docs/errors#SCOPE_REQUIRED", "request_id": "req_6f1c2a9b0d4e8f7a1b2c3d4e", "docs": "https://ollav.app/docs/agents#errors" } } ``` | Code | Status | What to do | |---|---|---| | `AUTH_REQUIRED` | 401 | Send `Authorization: Bearer ol_live_...` (or `ol_test_...`). If you did, the key is wrong, revoked or its user left the workspace. Ask the user for a new key. | | `SCOPE_REQUIRED` | 403 | The key lacks a scope (`upload`, `read`, `comment`). Ask the user for a key with it. | | `CSRF_REQUIRED` | 403 | Add `X-Ollav-Client: agent` to every POST, PATCH and DELETE. | | `NOT_FOUND` | 404 | Check the slug or id. A key only sees its own workspace, so other workspaces' documents also return 404. | | `ARCHIVED` | 404 | The document was archived. `POST /api/documents/{slug}/unarchive` restores it. | | `TOO_LARGE` | 413 | Over 15 MB. Compress images (WebP, downscale) and upload again. | | `NOT_HTML`, `EMPTY_BODY`, `MISSING_HTML`, `MISSING_FILE` | 400 | Send a non-empty HTML document as `file=@...`, a JSON `html` field or a `text/html` body. | | `VALIDATION_ERROR`, `BAD_JSON` | 400 | Fix the request body. The message names the field. | | `ROLE_REQUIRED`, `FORBIDDEN` | 403 | The user's role doesn't allow it. Tell the user instead of retrying. | | `PLAN_LIMIT`, `PLAN_READ_ONLY` | 402 | The workspace is at a limit of its plan (`limit` says which), or over one and read-only. Nothing was changed. Tell the user instead of retrying. | | `RATE_LIMITED`, `TOO_MANY_REQUESTS` | 429 | Wait and retry later (a test key allows 60 requests a minute). Don't loop. | | `IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_KEY_IN_USE` | 409 | The `Idempotency-Key` was used for a different request, or its first request is still running. Use a new key for a new request. | **Retries.** Send `Idempotency-Key: ` on every POST, PATCH and DELETE (MCP: the `idempotency_key` argument). If a call times out, retry it with the same key and the same body: it happens once, and the retry returns the first result. Keys last 24 hours. **Versions.** Breaking changes ship as dated API versions. Your key is pinned to the version current when it was created; send `Ollav-Version: 2026-11-01` to choose one explicitly. Every response names its version in the `Ollav-Version` header. See the [changelog](https://ollav.app/docs/changelog). ## All docs {#all-docs} - [MCP server](https://ollav.app/docs/mcp): connect, auth, verify, every tool, troubleshooting - [Quickstart: use Ollav from your agent via MCP](https://ollav.app/docs/quickstart/mcp) - [Quickstart: publish your first prototype with curl](https://ollav.app/docs/quickstart/curl) - [REST API reference](https://ollav.app/docs/api): every endpoint with a curl example - [Errors](https://ollav.app/docs/errors): every error code, its cause and fix - [Changelog](https://ollav.app/docs/changelog): API changes and versions ## Machine-readable references - [llms.txt](https://ollav.app/llms.txt), the index for LLMs, and [llms-full.txt](https://ollav.app/llms-full.txt), everything in one file - [OpenAPI 3.1](https://ollav.app/openapi.json), every endpoint an API key can call - [MCP server card](https://ollav.app/.well-known/mcp.json), the MCP endpoint, transport and auth - [Full API reference](https://app.ollav.app/llms.txt) with curl examples for every endpoint - [This page as Markdown](https://ollav.app/docs/agents.md) --- Page: https://ollav.app/docs/quickstart/mcp (updated 2026-10-27) # Use Ollav from your agent via MCP Connect your agent once and it can share an HTML page as a live link, read the comments people pin to it and publish the fix at the same link. This takes about five minutes with a **test key**, so nothing touches your team's live documents or your plan. ## 1. Get a test key {#key} Open https://app.ollav.app/keys, choose **Test**, keep the scopes `upload`, `read` and `comment`, and create the key. It is shown once. ```bash export OLLAV_API_KEY=ol_test_... ``` A test key works in your own sandbox: documents carry a TEST mark and are deleted after 30 days, there are no plan limits, and it may make 60 tool calls a minute. ## 2. Connect your client {#connect} ### Claude Code ```bash claude mcp add --transport http ollav https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY" claude mcp list ``` `ollav` shows as connected. Or install the plugin, which adds a skill telling Claude when to use Ollav: `/plugin marketplace add ollav-app/ollav`, then `/plugin install ollav@ollav`. ### Cursor `~/.cursor/mcp.json`: ```json { "mcpServers": { "ollav": { "url": "https://app.ollav.app/mcp", "headers": { "Authorization": "Bearer ${env:OLLAV_API_KEY}" } } } } ``` Restart Cursor from the shell where `OLLAV_API_KEY` is set. **Settings → MCP** shows `ollav` with a green dot. ### ChatGPT ChatGPT connects with OAuth, which gives it a live key. To try test mode first, create the app with the key in the URL: ```text Settings → Apps & Connectors → Create (developer mode) Name: Ollav MCP server URL: https://app.ollav.app/mcp/ol_test_YOUR_KEY Authentication: No authentication ``` That URL contains the key, so treat it like a password. When you're ready to share with your team, recreate the app with `https://app.ollav.app/mcp` and **OAuth**. Claude.ai, Claude Desktop, Codex, VS Code and other clients: see [Connect your client](https://ollav.app/docs/mcp#connect). ## 3. Share something {#share} Ask your agent: > Make a one-page pricing mockup in HTML and share it on Ollav. It calls `share_html` and gives you a link like `https://content.ollav.app/p/qkjgjQCQGz`. Open it: the page has a TEST mark, and you can click any element to pin a comment. Leave one, say on the main button: "Make this say Start free trial". A coding agent with a shell uploads a file on disk with curl instead of passing the HTML through the model, which costs no tokens. The `share_html` description tells it how. ## 4. Have it act on the review {#review} > Read the review comments on that page and fix them. The agent calls `get_review` (the open threads as Markdown and JSON, each with the element's text so it can find it in the source), edits the page, calls `publish_new_version` with the same slug (same link, comments kept) and `resolve_comment` on the thread, often replying with `add_comment` to say what changed. Reload the link to see the new version. That's the loop: share, review, publish a new version, resolve. ## 5. Check it, or fix it {#check} To confirm the key and endpoint outside the client: ```bash curl -s https://app.ollav.app/mcp \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` You get 11 tools. If the client reports an error, every Ollav error carries a `code`, a `hint` the agent can follow and a `doc_url`. The common ones: | Code | Why | Fix | |---|---|---| | `AUTH_REQUIRED` | No key sent, or the key is mistyped or revoked | Check the header or URL in the client config, and that the client sees `OLLAV_API_KEY` | | `NOT_FOUND` | A test key asked for a live document (or the other way round) | Use the same key that shared it | | `RATE_LIMITED` | Over 60 tool calls a minute with a test key | Wait a minute | Everything else: [Troubleshooting MCP](https://ollav.app/docs/mcp#troubleshooting). ## Next {#next} - Switch the client to a live key (`ol_live_...`), or OAuth, to share with your team. - Every tool, its inputs, outputs and errors: [the MCP docs](https://ollav.app/docs/mcp). - No MCP? [Publish your first prototype with curl](https://ollav.app/docs/quickstart/curl). --- Page: https://ollav.app/docs/quickstart/curl (updated 2026-10-27) # Publish your first prototype with curl Five commands, about five minutes. You publish an HTML file, get a link people can comment on, read their comments, ship a fix at the same link and close the thread. Everything here runs with a **test key**, so nothing touches your team's live documents or your plan. ## 1. Get a test key {#key} Open https://app.ollav.app/keys, choose **Test**, keep the scopes `upload`, `read` and `comment`, and create the key. It is shown once. ```bash export OLLAV_API_KEY=ol_test_... ``` Check it works: ```bash curl -s https://app.ollav.app/api/me -H "Authorization: Bearer $OLLAV_API_KEY" ``` The response says `"livemode": false`: this key works in your own sandbox. Test documents carry a TEST mark and are deleted after 30 days, and a test key may make 60 requests a minute. ## 2. Publish a file {#publish} Any self-contained HTML file up to 15 MB works. No file handy? Make one: ```bash cat > prototype.html <<'HTML' Pricing page

Pricing

Pro: €12 a month.

HTML ``` Upload it. curl reads the file from disk, which is also the cheapest way for an agent with a shell to publish: ```bash curl -s -F "file=@prototype.html" \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "X-Ollav-Client: agent" \ -H "Idempotency-Key: $(uuidgen)" \ https://app.ollav.app/api/documents ``` ```json { "url": "https://content.ollav.app/p/qkjgjQCQGz", "slug": "qkjgjQCQGz", "versionId": "...", "deduped": false, "versioned": false, "warnings": [], "livemode": false } ``` Open the `url`: that link is the deliverable. Keep the `slug`: ```bash export SLUG=qkjgjQCQGz ``` Every POST, PATCH and DELETE needs `X-Ollav-Client: agent`. `Idempotency-Key` is optional: retry with the same key and the upload happens once. ## 3. Leave a comment {#comment} Normally a reviewer opens the link and clicks an element. To try the loop alone, pin one yourself: ```bash curl -s -X POST https://app.ollav.app/api/comments \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "X-Ollav-Client: agent" \ -H "Content-Type: application/json" \ -d "{\"slug\":\"$SLUG\",\"body\":\"Make the button say Start free trial\",\"selector\":\"#buy\",\"rect\":{\"x\":0,\"y\":0,\"w\":1,\"h\":1},\"elementText\":\"Buy now\"}" ``` ## 4. Read the feedback {#review} `/api/review` returns the open threads. With `format=markdown` they come as a summary an agent can act on directly: ```bash curl -s "https://app.ollav.app/api/review?slug=$SLUG&format=markdown" -H "Authorization: Bearer $OLLAV_API_KEY" ``` Without `format`, the JSON has each thread's `id`, `comment` and `anchor.quote` (the element's text, for finding it in your source), plus a `cursor`: pass it as `since` next time to get only new activity. Keep the first thread's id (this uses `jq`): ```bash export COMMENT_ID=$(curl -s "https://app.ollav.app/api/review?slug=$SLUG" -H "Authorization: Bearer $OLLAV_API_KEY" | jq -r '.threads[0].id') ``` ## 5. Ship the fix at the same link, and resolve {#version} Edit the file, then publish a new version. The URL stays the same and every comment is kept: ```bash sed -i.bak 's/Buy now/Start free trial/' prototype.html curl -s -F "file=@prototype.html" \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "X-Ollav-Client: agent" \ https://app.ollav.app/api/documents/$SLUG/versions ``` Close the thread: ```bash curl -s -X POST https://app.ollav.app/api/comments/$COMMENT_ID/resolve \ -H "Authorization: Bearer $OLLAV_API_KEY" \ -H "X-Ollav-Client: agent" ``` ## When something fails {#errors} Errors are JSON with a stable `code`, a `hint` saying what to do and a `doc_url`. The common ones here: | Code | Why | Fix | |---|---|---| | `AUTH_REQUIRED` | The key is missing, mistyped or revoked | Check `$OLLAV_API_KEY` is exported in this shell | | `CSRF_REQUIRED` | A POST without `X-Ollav-Client` | Add `-H "X-Ollav-Client: agent"` | | `NOT_FOUND` | Wrong slug or id, or a live document with a test key | Use the slug from step 2 with the same key | | `TOO_LARGE` | Over 15 MB | Compress inlined images | | `RATE_LIMITED` | Over 60 requests a minute with a test key | Wait a minute | Every code: [errors](https://ollav.app/docs/errors). ## Next {#next} - Switch to a live key (`ol_live_...`) to share with your team. Live documents count towards your plan. - Let your agent do this: [Use Ollav from your agent via MCP](https://ollav.app/docs/quickstart/mcp). - Every endpoint: [REST API reference](https://ollav.app/docs/api). --- Page: https://ollav.app/security (updated 2026-10-26) # Security and privacy Ollav hosts HTML that people and AI agents generate, which means arbitrary JavaScript. The design starts from that. This page describes what is actually built today, with no promises we haven't shipped. ## Your data stays in the EU - Uploaded documents and comment images are stored in **Cloudflare R2 with EU jurisdiction**. Metadata (accounts, comments, versions) is in **Cloudflare D1 with EU jurisdiction**. Both are set when the store is created and can't be moved out later. - Nightly database backups go to an EU-jurisdiction bucket and are **deleted after 28 days**, so erased data leaves the backups within 30 days. - Cloudflare is a US company. EU jurisdiction controls where data is physically stored, not which law Cloudflare is subject to. We say this plainly because buyers ask. ## Your content is never used to train AI models Ollav doesn't train models on your documents, comments or images, and doesn't send them to any model provider. When *your* agent reads comments through the API, it does so with your API key, under your agreement with that provider. ## Untrusted HTML runs on a separate origin - Documents are served from **a separate content origin** (`content.ollav.app`), never from the app origin that holds sessions and API keys. A prototype's own JavaScript can't read your Ollav session. - The commenting overlay injected into each document is **credential-free**. It draws pins and works out which element you clicked. Authenticated calls happen in an app-origin frame that talks to it only through `postMessage` with a strict target origin. - Served documents get `X-Content-Type-Options: nosniff`, a `frame-ancestors` policy and `noindex`, so search engines don't index customer documents. - Comment images are checked by their file signature (PNG, JPEG, WebP or GIF; SVG is refused) and served only from the content origin, behind a short-lived token that's re-checked on every request. ## Access control - **Workspaces** with roles: owner, admin, member and guest. A guest sees only the documents shared with them. - Each document has its own visibility: **workspace** (default, members only), **link** (anyone signed in who has the URL) or **public** (anyone with the URL). Only members can comment, whatever the visibility. - Access is **re-checked on every request**. When someone leaves a workspace, their session and API keys stop reaching its documents straight away. Unknown, archived and forbidden documents all return the same 404, so an outsider can't tell which documents exist. - **API keys** for agents have scopes (`upload`, `read`, `comment`), are pinned to one workspace, are stored only as SHA-256 hashes, are shown once, and can be revoked at any time. - Sign-in uses Google (with full ID-token verification) or a magic link. Sessions are HttpOnly, Secure, SameSite=Lax cookies. ## GDPR: export and deletion, self-serve - **Export (Art. 15 and 20).** "Download my data" gives any user a ZIP of their profile, memberships, documents with every version, comments, reactions and images. Workspace owners can export the whole workspace. Agents can do the same with `POST /api/exports` or the `export_data` MCP tool. - **Deletion (Art. 17).** Delete a document, a workspace or your account from the Account page. Stored files nothing else references are removed straight away, and any that fail are retried nightly. An audit log keeps ids and counts, never content, for 365 days. ## Questions For security questions, a DPA or a vulnerability report, write to **security@ollav.app**. See also the [privacy policy](https://ollav.app/privacy) and [terms](https://ollav.app/terms). --- Page: https://ollav.app/terms (updated 2026-10-28) # Terms of Service Version 2026-10-20 These terms are the agreement between you and **[LEGAL ENTITY NAME]** ("Ollav", "we", "us"), of [REGISTERED ADDRESS], about your use of Ollav at ollav.app, app.ollav.app and content.ollav.app, its API and its MCP server (together, "the service"). By creating an account or using the service you agree to them. Questions: **[CONTACT EMAIL]**. How we handle personal data is in the [Privacy Policy](https://ollav.app/privacy). ## Accounts - You sign in with Google or with a link we email you. Keep access to that Google account or mailbox safe: whoever controls it controls your Ollav account. - You must be at least [MINIMUM AGE] years old to use the service. - You're responsible for what happens under your account, including the API keys you create and the people you invite to your workspaces. - If you use Ollav for an organisation, you confirm you're allowed to accept these terms for it. ## Plans and billing - Ollav has a free plan and paid plans. The current plans, limits and prices are on the [pricing page](https://app.ollav.app/pricing). - Paid plans are sold through **Stripe**, which acts as the **merchant of record** (Stripe Managed Payments). You buy the subscription from Stripe; Stripe takes your payment, holds your payment details, and calculates and collects VAT and sales tax. Stripe's own terms apply to the purchase. - Subscriptions renew automatically each month or year until cancelled. The Team plan is billed per member, and the amount follows the number of members (guests are free). - You can change plan or cancel at any time under **Workspace → Billing → Manage billing**. A cancellation takes effect at the end of the period you've paid for. - Refunds follow the Stripe Managed Payments setup and applicable consumer law. [TODO: Darragh to confirm the refund policy.] - If a payment fails, the paid limits stay in place for 14 days. After that, or after you cancel or downgrade, the workspace gets the free plan's limits. A workspace over a limit becomes read-only: we don't delete anything because of it. - We may change prices. We'll tell you before a change applies to your subscription, and you can cancel before it does. ## Your content - **You own what you upload**: HTML documents, comments, images and anything else ("your content"). We don't claim any ownership of it. - You give us a licence to store, copy, process and display your content **only to run the service for you**: to host it, show it to the people you've chosen to share it with, back it up and keep it secure. This licence ends when you delete the content, except for backup copies, which expire as described in the Privacy Policy. - We don't use your content to train AI models, and we don't send it to model providers. - You confirm you have the rights to upload and share your content, and that it doesn't break these terms or the law. ## Public pages and sharing - Each document has a visibility: **workspace** (members only), **link** (anyone signed in who has the URL) or **public** (anyone with the URL, no sign-in). You choose it, and you're responsible for what you make public. - Anyone who can see a document can copy what's in it. We can't undo that after you change the visibility or delete the document. - Documents are served from content.ollav.app, a separate origin, and run their own JavaScript in the visitor's browser. We don't check or vouch for what an uploaded document does. ## AI agents, the API and MCP - You can let AI agents and other software use Ollav for you through the REST API, the MCP server, an API key or an OAuth connection. - **You're responsible for what your agents do with your keys and connections**, as if you had done it yourself, including what they upload, share, comment or delete. Give a key only the scopes it needs, and revoke it when you no longer need it. - Your agreement with an AI provider (for example, the company that runs your agent) is between you and that provider. When your agent reads your Ollav content, that provider handles it under its own terms. - The API has rate limits and daily quotas that depend on your plan. Don't try to get around them. ## Test mode - Test keys (`ol_test_`) work in a private sandbox that's separate from your live workspace and doesn't count towards your plan's limits. Test keys are limited to 60 requests a minute. - **Test documents are deleted automatically 30 days after upload.** Don't keep anything in test mode you want to keep. - Test mode is for building and testing integrations, not for hosting real content. ## Acceptable use You must not use the service, or let anyone else use it, to: - host or send **phishing** pages, or pages that imitate another organisation or person to deceive people; - **collect passwords, payment details or other credentials** under false pretences; - distribute **malware**, or code that attacks, exploits or tracks visitors without their consent; - host or share **illegal content**, including child sexual abuse material and terrorist content; - send **spam**, including through invites, shares or comment notifications; - **harass**, threaten or abuse anyone; - host content that **infringes** someone else's copyright, trade mark, privacy or other rights; - **abuse the service**: try to break or bypass its security or its isolation between documents and accounts, access data that isn't yours, overload it, get around plan limits or rate limits, resell it without our agreement, or use it as general file storage or a CDN. We may remove or block content, revoke API keys and suspend or close accounts that break these rules, where possible after telling you, and immediately when we need to protect people or the service. We may report illegal content to the authorities. To report abuse or content that infringes your rights, write to **[ABUSE EMAIL]**. [TODO: Darragh to confirm the abuse and notice address, and whether a formal notice-and-action process is needed for the EU Digital Services Act.] ## Availability The service is provided **"as is" and "as available"**. We work to keep it running and your content safe, but we don't promise it will always be available, error-free or never lose data. Keep your own copy of anything important; you can download all your data at any time from the Account page. We may change or remove features. If we stop offering the service altogether, we'll give you reasonable notice so you can export your data. ## Liability - Nothing in these terms limits liability that can't be limited by law, including for death or personal injury caused by negligence, for fraud, or your rights as a consumer. - Otherwise, we're not liable for indirect or consequential loss, or for loss of profits, revenue, data or goodwill. - Otherwise, our total liability to you for anything arising from the service is limited to the greater of the amount you paid us for the service in the 12 months before the claim and €100. [TODO: lawyer to confirm these limits, and how they apply to consumers as opposed to business customers.] ## Ending your use - You can stop using Ollav at any time. Cancel any subscription, then delete your account from the Account page. Deleting the account removes your documents and personal data as described in the Privacy Policy. - We may suspend or close your account if you seriously or repeatedly break these terms, or if the law requires us to. Where we can, we'll tell you first and give you the chance to export your data. - The sections on your content, liability and governing law continue to apply after the agreement ends. ## Changes to these terms We may update these terms. If a change matters to you, we'll ask you to agree to the new version the next time you sign in, and we'll update the version at the top of this page. If you don't agree, you can stop using the service and delete your account. ## Governing law These terms are governed by the laws of **Ireland**, and the courts of Ireland have jurisdiction over any dispute. If you're a consumer living elsewhere in the EU, you also keep the protection of the mandatory laws of your country and can bring a claim there. [TODO: Darragh and lawyer to confirm governing law.] ## Contact [LEGAL ENTITY NAME], [REGISTERED ADDRESS]. Email: **[CONTACT EMAIL]**. --- Page: https://ollav.app/privacy (updated 2026-10-28) # Privacy Policy Version 2026-10-20 This policy explains what personal data Ollav collects when you use ollav.app, app.ollav.app, content.ollav.app, the API and the MCP server, why we collect it, and what your rights are under the EU General Data Protection Regulation (GDPR). ## Who we are The data controller is **[LEGAL ENTITY NAME]**, of [REGISTERED ADDRESS], Ireland. Ollav is run by Darragh Flood. For anything about your data, write to **[PRIVACY EMAIL]**. [TODO: Darragh to confirm whether a Data Protection Officer is required; if one is appointed, add their contact details here.] When a workspace owner uses Ollav for their organisation, the documents and comments in that workspace belong to them. [TODO: lawyer to confirm whether Ollav acts as a processor for workspace content and needs a Data Processing Agreement for business customers.] ## What we collect **Account data** - Your **email address**. With Google sign-in we ask Google for your basic profile (`openid email profile`) and check that your email address is verified. We store only the email address, not your name or profile picture. - With email sign-in, we send you a single-use link that expires after 15 minutes. - The names of workspaces you create, your memberships and roles, invitations you send or accept (including the invited email address), and which version of these Terms and this Privacy Policy you agreed to, and when. **Your content** - The **HTML documents** you upload, with every version, their titles and file names and when they were uploaded. - **Comments**, replies, reactions, the images you attach to comments, and the page state saved with a comment so a reviewer can see what you saw. **API keys and agent access** - For each API key: its name, the first 12 characters, a one-way hash of the key (never the key itself), its scopes, live or test mode, when it was created and last used, and when it was revoked. OAuth connections from AI clients create the same kind of key. **Usage and technical data** - Sign-in sessions: when each started, when it was last used and when it expires. - Counters used to limit how often actions can be repeated (for example, sign-in links per email address, API calls per day). - An audit log of exports and deletions, which holds ids and counts, never content. - Our hosting provider, Cloudflare, processes your **IP address**, browser details and the requests you make, to deliver the service and protect it from attacks. Our own database doesn't store IP addresses, except briefly in a counter that limits password sign-in attempts. Error logs can contain request details. [TODO: Darragh to confirm which Cloudflare logs are kept, and for how long.] **Billing data** - Paid plans are sold by **Stripe** as merchant of record. Stripe collects and holds your payment details, billing address and tax information; we never see your full card number. We keep the Stripe customer and subscription ids, the plan, its status, seat count and billing period for the workspace. **Emails** - When email is enabled, we send sign-in links, invitations, notifications about comments on documents, and warnings before a document expires. We don't use analytics, advertising or tracking tools, and we don't sell your data. We don't use your content to train AI models, and we don't send it to model providers. ## Why we use it, and the legal basis | Purpose | Legal basis | |---|---| | Creating your account, signing you in, hosting and showing your documents, comments, sharing, the API and MCP server, sending emails the service depends on | Performance of our contract with you (Art. 6(1)(b)) | | Billing, tax and accounting records | Performance of contract, and legal obligation (Art. 6(1)(c)) | | Keeping the service secure: rate limits, preventing abuse, enforcing the Acceptable Use rules, audit logs, backups | Our legitimate interest in running a safe, reliable service (Art. 6(1)(f)) | | Responding to your requests and complying with the law | Legal obligation (Art. 6(1)(c)) | ## Who processes your data We use these providers. Each processes data only as needed for its part of the service: - **Cloudflare** (hosting, database, file storage, security): [privacy policy](https://www.cloudflare.com/privacypolicy/) - **Stripe** (payments, tax; merchant of record for paid plans, so it is responsible for the payment data it collects): [privacy policy](https://stripe.com/privacy) - **Google** (Google sign-in): [privacy policy](https://policies.google.com/privacy) - **Resend** (sending email): [privacy policy](https://resend.com/legal/privacy-policy) - **GitHub** (runs the nightly database backup job; the backup passes through a GitHub-hosted runner on its way to our EU storage and isn't kept there): [privacy statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) When your own AI agent reads your Ollav content with your API key, the company that runs that agent handles it under your agreement with them, not ours. [TODO: Darragh to confirm the final list of processors and sign a Data Processing Agreement with each.] ## Where your data is stored Documents, comment images, the database and its backups are stored by Cloudflare in the **European Union** (EU jurisdiction for R2 and D1). Cloudflare, Stripe, Google, Resend and GitHub are, or belong to, US companies, so some data (for example, request data passing through Cloudflare's network, emails sent through Resend and the backup passing through GitHub) can be processed outside the EU. Where that happens, the transfer is covered by the EU-US Data Privacy Framework or the European Commission's Standard Contractual Clauses. [TODO: Darragh to confirm the transfer safeguard for each provider, and Resend's sending region.] ## How long we keep it - **Your account and content**: for as long as you keep them. You can delete documents, workspaces and your account yourself. - **Documents**: a live document expires 90 days after upload unless you extend it or mark it permanent. When email is enabled, we send a warning first. An expired document is archived: it is no longer served, but it stays stored until you delete it or delete your account. [TODO: Darragh to decide whether archived documents are deleted after a set time.] - **Test documents** (uploaded with an `ol_test_` key) are **deleted automatically 30 days after upload**. - **Sessions** last up to 30 days; sign-in links 15 minutes; data export links 15 minutes. - **Deletion**: when you delete a document, a workspace or your account, its database records are removed at once, and its stored files are removed straight away (any that fail are retried every night). Deleting your account also cancels your workspaces' subscriptions, and removes your sandboxes and your comments. A top-level comment someone else replied to is kept as "[deleted]" by "Deleted user", so the reply still makes sense. You can't delete your account while you're the only owner of a workspace other people use: transfer it first. - **Backups** are kept for 28 days, so deleted data leaves them within 30 days. - **Audit log** entries (ids and counts, no content) are kept for 365 days. - **Billing records** are kept by Stripe, and by us as long as the law requires for tax and accounting. [TODO: Darragh to confirm retention periods for billing records and logs.] ## Your rights Under the GDPR you have the right to: - **access** the personal data we hold about you, and get a copy; - **export** it in a machine-readable format (portability); - have it **corrected** if it is wrong; - have it **deleted**; - **restrict** or **object to** our use of it, including use based on our legitimate interests. Most of this is self-serve: **Account → Download my data** gives you a ZIP of your profile, documents and every version, comments, reactions and images, and **Account → Delete my account** deletes it. For anything else, write to **[PRIVACY EMAIL]**. We'll reply within one month, and may need to confirm it's you first. You also have the right to complain to the **Data Protection Commission** in Ireland ([dataprotection.ie](https://www.dataprotection.ie)), or to the data protection authority where you live. We'd appreciate the chance to sort it out with you first. ## Cookies and local storage Ollav uses only what it needs to work. There are no analytics or advertising cookies, so we don't ask for cookie consent. | Name | Where | What it does | How long | |---|---|---|---| | `ollav_session` | app.ollav.app | Keeps you signed in | Until you sign out or close the browser, at most 30 days | | `ollav_oauth` | app.ollav.app | Protects the Google sign-in step | 10 minutes | | `ollav_onboard` | app.ollav.app | Remembers you between signing in and agreeing to these terms | 30 minutes | | `ollav_view_{document}` | content.ollav.app | Lets you view a document that isn't public | Until you close the browser | | `ol-theme` (local storage) | your browser | Remembers light or dark mode | Until you clear it | | `ol-emoji-recent` (local storage) | your browser | Remembers emoji you used recently | Until you clear it | Uploaded documents are other people's HTML and may set their own cookies or load third-party content; that is outside our control. Stripe's checkout and billing pages set their own cookies under Stripe's policy. [TODO: Darragh to confirm no analytics or other cookies are added before launch.] ## Children Ollav isn't meant for children. You must be at least [MINIMUM AGE] to use it. ## Changes to this policy If we change this policy in a way that matters, we'll ask you to agree to the new version the next time you sign in, and update the version at the top of this page. ## Contact [LEGAL ENTITY NAME], [REGISTERED ADDRESS], Ireland. Email: **[PRIVACY EMAIL]**. See also the [Terms of Service](https://ollav.app/terms) and our [security overview](https://ollav.app/security). --- Page: https://ollav.app/guides/feedback-on-ai-generated-prototypes (updated 2026-10-26) # How to get feedback on an AI-generated prototype An assistant can produce a clickable prototype in minutes. Getting useful feedback on it takes longer. A screenshot loses the interactions, a zip file never gets opened, and "looks good 👍" in chat doesn't tell anyone what to change. Here's a workflow that gets specific feedback and feeds it back to the agent. ## 1. Make it one self-contained HTML file Reviewers should open a link and see the real thing. That works when the prototype is **a single HTML file** with its CSS, JavaScript and images inlined, or loaded from absolute `https://` URLs. - If the assistant wrote one file, you're done. - If it built a React or Vue app, have the bundler emit one file. With Vite that's `vite-plugin-singlefile`. See [single-file HTML builds](https://ollav.app/guides/single-file-html-builds). - Avoid relative paths like `src="assets/logo.png"`. Nothing else gets uploaded with the file, so they won't load. ## 2. Share a live link, not a picture Upload the file to Ollav: drag it onto https://app.ollav.app, or let your agent do it with the `share_html` MCP tool or one curl call: ```bash curl -F "file=@prototype.html" -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" https://app.ollav.app/api/documents ``` You get a stable link. Workspace members can open it straight away, and you can share a single document with an outside reviewer as a guest. ## 3. Ask for feedback on elements, not in a thread Ask reviewers to **click the thing they're talking about** and comment there. Each comment is pinned to that element, so "this" always means something. Make the request specific: - "Comment on anything that would stop you finishing checkout." - "React 👍 on comments you agree with instead of repeating them." - "Resolve your own comment if a later version fixes it." Reactions keep the noise down: five people agreeing is one comment with 👍 5, not five replies. ## 4. Feed the comments back to the agent The agent that built the prototype can read the feedback directly. `get_review` returns the open threads as Markdown, each with the text of the element it's pinned to, so the agent can find it in the source: ```text Read the open comments on the Ollav prototype, fix them, publish a new version and resolve what you fixed. ``` Under the hood that's `get_review` → edit → `publish_new_version` → `resolve_comment`. The link stays the same and earlier comments are kept and marked as being on an older version. ## 5. Repeat until it's quiet Each round, reviewers reload the same link. When `get_review` comes back empty, you're done. The link and the full discussion stay with the work, for whoever picks it up next. ## Checklist - One HTML file, everything inlined or absolute - Shared as a live link with the right visibility (workspace by default) - Reviewers comment on elements and react instead of repeating - The agent reads open threads, ships a new version, resolves what it fixed [Connect your agent](https://ollav.app/docs/agents) · [Get started](https://app.ollav.app/signin) --- Page: https://ollav.app/guides/share-claude-artifact-html-with-your-team (updated 2026-10-26) # Share an AI-made HTML artifact with your team Assistants are good at producing small interactive pages: a dashboard mock, a pricing calculator, a one-page explainer. Getting colleagues to *see it working* is the awkward part. This guide covers sharing that kind of single-file HTML "artifact" as a live link your team can comment on. ## What works Ollav hosts **one self-contained HTML file up to 15 MB**. Most assistant-made artifacts already fit: - Inline `