DocsUpdated 2026-10-27View as Markdown

REST API reference

Every endpoint an API key can call, generated from the OpenAPI 3.1 spec (version 1.3.0), which CI checks against the running API. Prefer MCP from an agent? See the MCP docs. New here? Publish your first prototype with curl.

Basics

  • Base URL: https://app.ollav.app. Shared documents are served from https://content.ollav.app/p/{slug}.
  • Auth: Authorization: Bearer $OLLAV_API_KEY. The user creates the key at https://app.ollav.app/keys; it belongs to one workspace and has the scopes upload, read and comment. ol_test_... keys work in the user's test sandbox: no plan limits, 60 requests a minute, documents deleted after 30 days, and responses say "livemode": false. ol_live_... keys (and older sk_live_... keys) work on live data.
  • **X-Ollav-Client: agent** on every POST, PATCH and DELETE (a CSRF guard; any value works). Without it: 403 CSRF_REQUIRED.
  • **Idempotency-Key** (optional, any unique string) on POST, PATCH and DELETE makes a retry safe: it runs once and replays the first response for 24 hours.
  • **Ollav-Version** (optional) picks a dated API version; otherwise the version your key was pinned to is used. See the changelog.
  • Errors are JSON {"error": {"type", "code", "message", "param", "hint", "doc_url", "request_id"}}. Match on code; every code is on the errors page. Every response has a Request-Id header.
  • Examples use $OLLAV_API_KEY; set it to a test key while you build: export OLLAV_API_KEY=ol_test_....

Endpoints

Documents

A document is one self-contained HTML file (up to 15 MB) at a stable URL on the content origin. Uploading a file with the same filename as one you already own adds a version instead of a new document.

List documents

GET /api/documents

Lists your non-archived documents in the key's workspace, newest first, as a bare array. Use GET /api/dashboard instead when you also need version and comment counts. Guests can't list (403 ROLE_REQUIRED).

Responses

StatusMeaning
200Your documents (a bare array).
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
429RateLimited
500InternalError
curl "https://app.ollav.app/api/documents" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

[
  {
    "id": "9908f969-9774-4374-a5bf-d66abcfbe70b",
    "slug": "qkjgjQCQGz",
    "title": "Pricing page v2",
    "filename": "pricing.html",
    "visibility": "workspace",
    "createdAt": 1793107356443,
    "url": "https://content.ollav.app/p/qkjgjQCQGz"
  }
]

Upload (share) an HTML document

POST /api/documents

MCP tool: share_html

Publishes one self-contained HTML file and returns its share url. Give that url to the user. Send JSON {html, title?, filename?, visibility?}, multipart/form-data with a file field, or a raw text/html body (title, filename and visibility then come from the query string or X-Title/X-Filename/X-Visibility headers). If you already own a document with the same filename and the content changed, this adds a version to it (201, versioned: true); identical content returns the existing version (200, deduped: true). To update a known document prefer POST /api/documents/{slug}/versions. warnings lists relative asset paths that won't load; it never blocks the upload. New documents expire after 90 days unless extended or made permanent. Publishing (this and new versions together) is limited to 30 an hour per account (looser per IP): over it, 429 RATE_LIMITED with error.retry_after in seconds. A multipart upload takes one file (400 TOO_MANY_FILES).

Request body (application/json, multipart/form-data, text/html)

FieldTypeRequiredDescription
htmlstringyesThe whole self-contained HTML document, up to 15 MB.
titlestringDefaults to the <title> tag, then the filename.
filenamestringRe-uploading the same filename adds a version to your document.
visibilityworkspace | link | publicworkspace (default): members of the workspace only. link: anyone signed in to Ollav with the URL. public: PUBLIC, anyone with the URL, no sign-in, including people outside the company.

Responses

StatusMeaning
200Deduplicated. You already uploaded exactly this content; nothing changed.
201A new document, or a new version of the document with the same filename.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
409Conflict
413PayloadTooLarge
429RateLimited
500InternalError
curl -X POST "https://app.ollav.app/api/documents" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"html":"<!DOCTYPE html><html><head><title>Pricing page v2</title></head><body><h1>Pricing</h1></body></html>","filename":"pricing.html","visibility":"workspace"}'

Response (200):

{
  "url": "https://content.ollav.app/p/qkjgjQCQGz",
  "slug": "qkjgjQCQGz",
  "documentId": "9908f969-9774-4374-a5bf-d66abcfbe70b",
  "versionId": "b8cfa354-0c7c-4102-bf14-69b49de24ca3",
  "hash": "859d1148aaf2854ea2a59a25b89063395b0ded22230bdb4c7aad72d2a83b537a",
  "deduped": true,
  "versioned": false,
  "warnings": [],
  "livemode": true
}

Get a document

GET /api/documents/{slug}

MCP tool: get_document

Returns one document's metadata with every stored version (newest first). For the document's owner or a workspace admin; anyone else gets the same 404 as for a missing slug. Archived documents are returned too (archivedAt is set). Versions older than the plan's retention window are hidden, not deleted.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Responses

StatusMeaning
200The document with its versions.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/documents/qkjgjQCQGz" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "id": "9908f969-9774-4374-a5bf-d66abcfbe70b",
  "workspaceId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
  "slug": "qkjgjQCQGz",
  "title": "Pricing page v2",
  "filename": "pricing.html",
  "ownerId": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
  "ownerEmail": "ada@example.com",
  "visibility": "workspace",
  "currentVersionId": "07120538-9e66-460f-9f26-22f3910a213a",
  "createdAt": 1793107356443,
  "updatedAt": 1793107356781,
  "archivedAt": null,
  "expiresAt": 1800883356443,
  "isPermanent": 0,
  "expiryWarnedAt": null,
  "url": "https://content.ollav.app/p/qkjgjQCQGz",
  "versions": [
    {
      "id": "07120538-9e66-460f-9f26-22f3910a213a",
      "workspaceId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
      "documentId": "9908f969-9774-4374-a5bf-d66abcfbe70b",
      "hash": "97602aa74d0b26c2fcc8494d28a82c13430712aeacc549f67b7ce3c1dcf74221",
      "r2Key": "docs/97602aa74d0b26c2fcc8494d28a82c13430712aeacc549f67b7ce3c1dcf74221",
      "size": 53,
      "createdAt": 1793107356781
    }
  ]
}

Change who can view a document

PATCH /api/documents/{slug}

Sets the document's visibility. Only use public when the user explicitly asked to share outside their company: anyone with the URL can then view it without signing in. Commenting stays members-only whatever the visibility. Owner or workspace admin; needs the upload scope.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Request body (application/json)

FieldTypeRequiredDescription
visibilityworkspace | link | publicyesworkspace (default): members of the workspace only. link: anyone signed in to Ollav with the URL. public: PUBLIC, anyone with the URL, no sign-in, including people outside the company.

Responses

curl -X PATCH "https://app.ollav.app/api/documents/qkjgjQCQGz" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"visibility":"link"}'

Response (200):

{
  "ok": true,
  "visibility": "link"
}

Delete a document (session only)

DELETE /api/documents/{slug}

Needs a signed-in browser session; an API key gets 401.

Permanently deletes the document, its versions, comments, reactions and images, and erases stored files nothing else references. Needs a signed-in browser session and the document's title or slug typed in confirm; an API key gets 403 FORBIDDEN. Agents: archive instead (POST /api/documents/{slug}/archive), or ask the user to delete it in the web UI.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Request body (application/json)

FieldTypeRequiredDescription
confirmstringyesThe document's title or slug.

Responses

StatusMeaning
200Deleted.
400BadRequest
401Unauthorized
403Forbidden
404NotFound
500InternalError
curl -X DELETE "https://app.ollav.app/api/documents/qkjgjQCQGz" \
  --cookie "ollav_session=$OLLAV_SESSION" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"confirm":"qkjgjQCQGz"}'

Response (200):

{
  "deleted": true,
  "r2": {
    "deleted": 3,
    "pending": 0
  }
}

Archive a document

POST /api/documents/{slug}/archive

Takes the document offline: its URL answers 410 and it leaves listings. Nothing is deleted; undo with POST /api/documents/{slug}/unarchive. Owner or workspace admin; needs the upload scope.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Responses

curl -X POST "https://app.ollav.app/api/documents/qkjgjQCQGz/archive" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent"

Response (200):

{
  "ok": true,
  "archived": true
}

Unarchive a document

POST /api/documents/{slug}/unarchive

Brings an archived document back online at the same URL. Counts against the plan's document limit again, so it can answer 402.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Responses

curl -X POST "https://app.ollav.app/api/documents/qkjgjQCQGz/unarchive" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent"

Response (200):

{
  "ok": true,
  "archived": false
}

Extend, set or remove a document's expiry

POST /api/documents/{slug}/expiry

Documents are archived automatically when they expire (90 days after upload by default). extend sets expiry to 90 days from now, permanent removes it, custom sets expiresAt (Unix ms, in the future). Test-mode documents are deleted 30 days after upload: for them extend sets that date, and permanent or a later custom date is a 400. Owner or workspace admin; needs the upload scope.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Request body (application/json)

FieldTypeRequiredDescription
actionextend | permanent | customyes
expiresAtintegerUnix ms; required when action is custom.

Responses

StatusMeaning
200The new expiry.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
409Conflict
429RateLimited
500InternalError
curl -X POST "https://app.ollav.app/api/documents/qkjgjQCQGz/expiry" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"action":"extend"}'

Response (200):

{
  "ok": true,
  "isPermanent": false,
  "expiresAt": 1800883357891
}

List documents with version and comment counts

GET /api/dashboard

MCP tool: list_documents

Your documents in the key's workspace, newest first, with version, comment and unresolved-comment counts: the quickest way to find which document has open feedback. Includes archived documents (archivedAt set). sharedWithMe is filled for browser sessions only.

Responses

StatusMeaning
200The key's workspace and its documents.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
429RateLimited
500InternalError
curl "https://app.ollav.app/api/dashboard" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "workspace": {
    "id": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
    "name": "Acme",
    "role": "owner"
  },
  "documents": [
    {
      "id": "9908f969-9774-4374-a5bf-d66abcfbe70b",
      "slug": "qkjgjQCQGz",
      "title": "Pricing page v2",
      "filename": "pricing.html",
      "visibility": "workspace",
      "createdAt": 1793107356443,
      "updatedAt": 1793107357969,
      "archivedAt": null,
      "expiresAt": 1800883356443,
      "isPermanent": 0,
      "versionCount": 3,
      "commentCount": 2,
      "unresolvedCount": 1,
      "url": "https://content.ollav.app/p/qkjgjQCQGz"
    }
  ],
  "sharedWithMe": []
}

Versions

Every upload of new content is a version. The URL never changes; comments on earlier versions are kept and marked as such.

List a document's versions

GET /api/documents/{slug}/versions

Every visible version, newest first, numbered from 1 (the first upload), with which one is current. Versions older than the plan's retention window are hidden, not deleted. Owner or workspace admin; needs the read scope.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Responses

StatusMeaning
200The versions.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/documents/qkjgjQCQGz/versions" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "document": {
    "slug": "qkjgjQCQGz",
    "title": "Pricing page v2",
    "versionCount": 2
  },
  "versions": [
    {
      "id": "07120538-9e66-460f-9f26-22f3910a213a",
      "hash": "97602aa74d0b26c2fcc8494d28a82c13430712aeacc549f67b7ce3c1dcf74221",
      "size": 53,
      "createdAt": "2026-10-27T13:22:36.781Z",
      "isCurrent": true,
      "versionNumber": 2
    },
    {
      "id": "b8cfa354-0c7c-4102-bf14-69b49de24ca3",
      "hash": "859d1148aaf2854ea2a59a25b89063395b0ded22230bdb4c7aad72d2a83b537a",
      "size": 99,
      "createdAt": "2026-10-27T13:22:36.443Z",
      "isCurrent": false,
      "versionNumber": 1
    }
  ]
}

Publish a new version

POST /api/documents/{slug}/versions

MCP tool: publish_new_version

Replaces what the document's URL shows with new HTML; the URL stays the same and earlier comments are kept, marked "earlier version". Use this after editing in response to feedback, then resolve the threads you fixed. Same body formats as POST /api/documents. Identical content to the current version returns 200 with deduped: true. Needs the upload scope; archived documents answer 404 ARCHIVED, documents taken down for breaking the Terms 410 DOCUMENT_REMOVED. Counts towards the 30 publishes an hour per account (429 RATE_LIMITED).

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Request body (application/json, multipart/form-data, text/html)

FieldTypeRequiredDescription
htmlstringyesThe full new HTML.
titlestringOverrides the <title> tag.

Responses

StatusMeaning
200Deduplicated. Same content as the current version; nothing changed.
201New version published.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
409Conflict
410Gone
413PayloadTooLarge
429RateLimited
500InternalError
curl -X POST "https://app.ollav.app/api/documents/qkjgjQCQGz/versions" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"html":"<!DOCTYPE html><html><head><title>Pricing page v3</title></head><body><h1 style='\''color:blue'\''>Pricing</h1></body></html>"}'

Response (200):

{
  "deduped": true,
  "slug": "qkjgjQCQGz",
  "url": "https://content.ollav.app/p/qkjgjQCQGz",
  "versionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
  "hash": "c0fb1a4309519139808296b940c600f23fa26a67fd1732f7610528fd95298909",
  "warnings": [],
  "livemode": true
}

Comments

Element-anchored comments and single-level replies. Top-level comments carry the CSS selector and bounding rect of the element they pin to.

List a document's comments

GET /api/comments

MCP tool: get_comments

Every comment thread on the document, resolved ones included, with replies, reactions and image attachments. For acting on feedback prefer GET /api/review, which returns open threads shaped for finding the element in your source. Needs the read scope.

Parameters

NameInTypeRequiredDescription
slugquerystringyesThe document's slug, from the upload response or GET /api/dashboard.

Responses

StatusMeaning
200Threads, oldest first.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/comments?slug=qkjgjQCQGz" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "slug": "qkjgjQCQGz",
  "currentVersionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
  "docOwnerId": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
  "viewerCanManage": true,
  "count": 1,
  "unresolvedCount": 1,
  "comments": [
    {
      "id": "e7cb98b7-c5e3-4048-8590-e7c1b1dea787",
      "documentId": "9908f969-9774-4374-a5bf-d66abcfbe70b",
      "versionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
      "parentId": null,
      "selector": "h1",
      "fallbackRect": {
        "x": 24,
        "y": 80,
        "w": 640,
        "h": 48
      },
      "elementText": "Pricing",
      "authorId": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
      "body": "Make the headline blue",
      "createdAt": "2026-10-27T13:22:38.297Z",
      "resolvedAt": null,
      "resolvedBy": null,
      "author": {
        "email": "ada@example.com",
        "name": "Ada"
      },
      "isCurrentVersion": true,
      "reproduceNote": null,
      "breadcrumbs": null,
      "markEmoji": null,
      "attachment": null,
      "snapshotUrl": null,
      "reactions": [
        {
          "reaction": "๐Ÿ‘",
          "count": 1,
          "mine": false
        }
      ],
      "replies": []
    }
  ]
}

Post a comment or reply

POST /api/comments

MCP tool: add_comment

A top-level comment pins to an element: send selector (CSS) and rect (its bounding box, page coordinates). A reply sends parentId (a top-level comment's id) and no selector or rect; replies are one level deep. markEmoji without body places an emoji mark as a reaction to the element. Members only, whatever the document's visibility; needs the comment scope. Limited to 120 comments and replies an hour per account (looser per IP): over it, 429 RATE_LIMITED with error.retry_after in seconds.

Request body (application/json)

FieldTypeRequiredDescription
slugstringyes
bodystringRequired, unless a top-level comment sets markEmoji.
parentIdstringSet for a reply (a top-level comment's id); omit for a top-level comment.
selectorstringCSS selector of the element. Required for top-level comments.
rectRectBounding box of the element, in page pixels.
elementTextstringThe element's visible text; helps re-anchor and find it in source.
markEmojistring or nullOne emoji to show as the pin (top-level only); null for a numbered pin.
reproduceNotestringHow to reach the UI state, for reviewers (top-level only).
breadcrumbsarray of BreadcrumbClicks that lead to the UI state (top-level only).

Responses

curl -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":"qkjgjQCQGz","body":"Make the headline blue","selector":"h1","rect":{"x":24,"y":80,"w":640,"h":48},"elementText":"Pricing"}'

Response (201):

{
  "id": "e7cb98b7-c5e3-4048-8590-e7c1b1dea787",
  "workspaceId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
  "documentId": "9908f969-9774-4374-a5bf-d66abcfbe70b",
  "versionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
  "parentId": null,
  "selector": "h1",
  "fallbackRect": {
    "x": 24,
    "y": 80,
    "w": 640,
    "h": 48
  },
  "elementText": "Pricing",
  "authorId": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
  "body": "Make the headline blue",
  "createdAt": "2026-10-27T13:22:38.297Z",
  "resolvedAt": null,
  "resolvedBy": null,
  "reproduceNote": null,
  "breadcrumbs": null,
  "markEmoji": null,
  "author": {
    "email": "ada@example.com",
    "name": "Ada"
  }
}

Delete a comment

DELETE /api/comments/{id}

MCP tool: delete_comment

Deletes a comment; deleting a top-level comment deletes its replies. Only the comment's author or the document's owner. Anyone else, and unknown ids, get the same 404. Needs the comment scope.

Parameters

NameInTypeRequiredDescription
idpathstringyesA comment id, from GET /api/review or GET /api/comments.

Responses

curl -X DELETE "https://app.ollav.app/api/comments/e7cb98b7-c5e3-4048-8590-e7c1b1dea787" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent"

Response (200):

{
  "deleted": true
}

Resolve a comment thread

POST /api/comments/{id}/resolve

MCP tool: resolve_comment

Marks a top-level comment resolved. Do this after publishing a version that addresses it, and reply saying what changed. The comment's author or the document's owner; needs the comment scope.

Parameters

NameInTypeRequiredDescription
idpathstringyesA comment id, from GET /api/review or GET /api/comments.

Responses

curl -X POST "https://app.ollav.app/api/comments/e7cb98b7-c5e3-4048-8590-e7c1b1dea787/resolve" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent"

Response (200):

{
  "ok": true,
  "resolved": true
}

Reopen a comment thread

POST /api/comments/{id}/unresolve

MCP tool: resolve_comment

Reopens a resolved top-level comment. Same rules as resolving (MCP: resolve_comment with resolved: false).

Parameters

NameInTypeRequiredDescription
idpathstringyesA comment id, from GET /api/review or GET /api/comments.

Responses

curl -X POST "https://app.ollav.app/api/comments/e7cb98b7-c5e3-4048-8590-e7c1b1dea787/unresolve" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent"

Response (200):

{
  "ok": true,
  "resolved": false
}

Attach an image to a comment

POST /api/comments/{id}/attachments

Attaches ONE image (a screenshot) to a top-level comment you wrote, or on a document you own. multipart/form-data with a file field, or the raw image as the body. PNG, JPEG, WebP or GIF, checked by content (SVG is refused), up to 5 MB. Served from the content origin to those who can view the document. Needs the comment scope.

Parameters

NameInTypeRequiredDescription
idpathstringyesA comment id, from GET /api/review or GET /api/comments.

Request body (multipart/form-data, image/png)

FieldTypeRequiredDescription
filestring (binary)yes
widthintegerDisplay width in px (optional).
heightintegerDisplay height in px (optional).

Responses

curl -X POST "https://app.ollav.app/api/comments/e7cb98b7-c5e3-4048-8590-e7c1b1dea787/attachments" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -F file=@screenshot.png

Response (201):

{
  "id": "b8993718-40ff-49bc-aed5-cf0040a4105e",
  "url": "https://content.ollav.app/a/b8993718-40ff-49bc-aed5-cf0040a4105e?vt=eyJ...",
  "mime": "image/png",
  "width": 1280,
  "height": 720,
  "size": 48213
}

Reactions

Emoji reactions on comments and replies.

Add or remove a reaction

POST /api/comments/{id}/reactions

MCP tool: react_to_comment

Adds (on: true, the default) or removes (on: false) your reaction on a comment or reply. Any single emoji; the legacy names +1, eyes, check and heart still work. A comment holds at most 20 different reactions. Anyone who may comment on the document; needs the comment scope.

Parameters

NameInTypeRequiredDescription
idpathstringyesA comment id, from GET /api/review or GET /api/comments.

Request body (application/json)

FieldTypeRequiredDescription
reactionstringyesOne emoji, e.g. ๐Ÿ‘.
onboolean

Responses

StatusMeaning
200The comment's reactions after the change.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
409Conflict
429RateLimited
500InternalError
curl -X POST "https://app.ollav.app/api/comments/e7cb98b7-c5e3-4048-8590-e7c1b1dea787/reactions" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"reaction":"๐Ÿ‘"}'

Response (200):

{
  "ok": true,
  "commentId": "e7cb98b7-c5e3-4048-8590-e7c1b1dea787",
  "reactions": [
    {
      "reaction": "๐Ÿ‘",
      "count": 1,
      "mine": true
    }
  ]
}

Review

The agent feedback loop: read open threads shaped for editing source, publish a new version, resolve what you fixed.

Read feedback to act on

GET /api/review

MCP tool: get_review

The feedback digest for an iterating agent: open threads by default, each with a source-findable anchor (quote = the element's text, reproduceNote, breadcrumbs). selector is the rendered DOM, not your source; search your source for quote. Pass the response's cursor back as since to get only threads with new activity. format=markdown returns the same digest as text/markdown. Loop: get_review โ†’ edit โ†’ publish_new_version โ†’ resolve_comment. Needs the read scope.

Parameters

NameInTypeRequiredDescription
slugquerystringyesThe document's slug, from the upload response or GET /api/dashboard.
formatqueryjson | markdownjson (default) or markdown (text/markdown, the same threads as a readable summary).
statusqueryopen | resolved | allWhich threads to return.
sincequerystringISO 8601 or epoch ms. Only threads with activity after this; pass the previous response's cursor.

Responses

StatusMeaning
200The digest.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/review?slug=qkjgjQCQGz" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "slug": "qkjgjQCQGz",
  "title": "Pricing page v2",
  "url": "https://content.ollav.app/p/qkjgjQCQGz",
  "currentVersionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
  "filter": {
    "status": "open",
    "since": null
  },
  "totalThreads": 1,
  "openThreads": 1,
  "returned": 1,
  "cursor": "2026-10-27T13:22:38.402Z",
  "generatedAt": "2026-10-27T13:22:38.836Z",
  "note": "anchor.selector targets the rendered DOM; to locate the element in your SOURCE (JSX/HTML) prefer anchor.quote and anchor.reproduceNote.",
  "threads": [
    {
      "id": "e7cb98b7-c5e3-4048-8590-e7c1b1dea787",
      "status": "open",
      "onCurrentVersion": true,
      "versionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
      "author": {
        "email": "ada@example.com",
        "name": "Ada"
      },
      "createdAt": "2026-10-27T13:22:38.297Z",
      "lastActivityAt": "2026-10-27T13:22:38.402Z",
      "comment": "Make the headline blue",
      "markEmoji": null,
      "attachment": null,
      "anchor": {
        "quote": "Pricing",
        "reproduceNote": null,
        "breadcrumbs": null,
        "selector": "h1"
      },
      "replies": []
    }
  ],
  "markdown": "# Review: Pricing page v2\n\n..."
}

Exports

Data exports (GDPR Art. 15/20) as JSON or a streamed ZIP.

Export one document as JSON

GET /api/documents/{slug}/export

One JSON bundle: metadata, every version with its HTML inlined, and every comment. For everything in an account or workspace (with images and snapshots) use POST /api/exports. Owner or workspace admin; needs the read scope.

Parameters

NameInTypeRequiredDescription
slugpathstringyesThe document's slug, from the upload response or GET /api/dashboard.

Responses

StatusMeaning
200The bundle.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/documents/qkjgjQCQGz/export" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "exportedAt": "2026-10-27T13:22:38.141Z",
  "document": {
    "id": "9908f969-9774-4374-a5bf-d66abcfbe70b",
    "slug": "qkjgjQCQGz",
    "title": "Pricing page v2",
    "filename": "pricing.html",
    "visibility": "workspace",
    "createdAt": "2026-10-27T13:22:36.443Z",
    "updatedAt": "2026-10-27T13:22:37.969Z",
    "archivedAt": null,
    "expiresAt": null,
    "isPermanent": true,
    "url": "https://content.ollav.app/p/qkjgjQCQGz"
  },
  "versions": [
    {
      "id": "f2d1c6e2-6960-488b-a198-6d4f33ab1841",
      "hash": "c0fb1a4309519139808296b940c600f23fa26a67fd1732f7610528fd95298909",
      "r2Key": "docs/c0fb1a4309519139808296b940c600f23fa26a67fd1732f7610528fd95298909",
      "size": 35,
      "createdAt": "2026-10-27T13:22:37.459Z",
      "isCurrent": true,
      "html": "<html><body><p>v3</p></body></html>"
    }
  ],
  "comments": [
    {
      "id": "e7cb98b7-c5e3-4048-8590-e7c1b1dea787",
      "parentId": null,
      "selector": "h1",
      "elementText": "Pricing",
      "authorId": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
      "body": "Make the headline blue",
      "createdAt": "2026-10-27T13:22:38.297Z",
      "resolvedAt": null,
      "resolvedBy": null,
      "versionId": "f2d1c6e2-6960-488b-a198-6d4f33ab1841"
    }
  ]
}

Export data as a ZIP

POST /api/exports

MCP tool: export_data

Returns a signed download link, valid 15 minutes, to a ZIP streamed on demand. scope account: your profile, memberships, owned documents with every version, your comments and reactions (an API key: its own workspace only, partial: true). scope workspace: everything in the workspace (owner only). manifest.json in the ZIP lists every item. Needs the read scope; limited to a few exports per hour (429).

Request body (application/json)

FieldTypeRequiredDescription
scopeaccount | workspace
workspaceIdstringDefault: the key's workspace.

Responses

StatusMeaning
201The download link.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
409Conflict
429RateLimited
500InternalError
curl -X POST "https://app.ollav.app/api/exports" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent" \
  -H "Content-Type: application/json" \
  -d '{"scope":"workspace"}'

Response (201):

{
  "scope": "workspace",
  "workspaceId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
  "downloadUrl": "https://app.ollav.app/api/exports/download?token=eyJ...",
  "expiresAt": 1793108258236,
  "partial": false
}

Download an export

GET /api/exports/download

No API key: the signed link is the credential.

The downloadUrl returned by POST /api/exports. The signed token is the credential (no API key needed); access is re-checked when the download starts. Streams application/zip. Expired or invalid tokens get 404: request a new export.

Parameters

NameInTypeRequiredDescription
tokenquerystringyesThe signed token from downloadUrl.

Responses

StatusMeaning
200The ZIP (Content-Disposition attachment).
400BadRequest
404NotFound
500InternalError
curl "https://app.ollav.app/api/exports/download?token=eyJ1aWQiOiJlOTcw..."

Workspaces

Workspaces own documents. An API key is pinned to one workspace; managing workspaces (create, invite, remove) needs a signed-in browser session.

List workspaces

GET /api/workspaces

The workspaces the caller belongs to (an API key: its user's memberships) and which is current; for an API key the current one is always the key's own. billingNotice is set when the current workspace's payment failed or it is over its plan's limits.

Responses

StatusMeaning
200Workspaces.
400BadRequest
401Unauthorized
402PlanLimit
429RateLimited
500InternalError
curl "https://app.ollav.app/api/workspaces" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "currentId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
  "workspaces": [
    {
      "id": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
      "name": "Acme",
      "personal": false,
      "role": "owner"
    }
  ],
  "billingNotice": null
}

List a workspace's members

GET /api/workspaces/{id}/members

Members with their roles (owner > admin > member > guest) and the pending invites the caller may see. Guests and non-members get 404. Inviting and removing members needs the web UI.

Parameters

NameInTypeRequiredDescription
idpathstringyesA workspace id, from GET /api/workspaces or GET /api/me.

Responses

StatusMeaning
200Members and pending invites.
400BadRequest
401Unauthorized
402PlanLimit
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/workspaces/0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90/members" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "workspaceId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
  "role": "owner",
  "members": [
    {
      "userId": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
      "email": "ada@example.com",
      "name": "Ada",
      "role": "owner",
      "joinedAt": 1793107356443
    }
  ],
  "invites": []
}

Billing

Read-only plan, usage and limits for a workspace. Plan changes happen in the web UI through Stripe Checkout and the Customer Portal.

Get a workspace's plan, usage and limits

GET /api/workspaces/{id}/billing

The workspace's plan, subscription, usage and limits (null = no limit), and which limits it is over. A workspace over a limit is read-only: reads work, writes that grow it answer 402. When billing isn't configured, enabled is false and there are no limits. Plan changes happen in the web UI (https://app.ollav.app/pricing). Members of the workspace; needs the read scope.

Parameters

NameInTypeRequiredDescription
idpathstringyesA workspace id, from GET /api/workspaces or GET /api/me.

Responses

StatusMeaning
200Billing overview.
400BadRequest
401Unauthorized
402PlanLimit
403Forbidden
404NotFound
429RateLimited
500InternalError
curl "https://app.ollav.app/api/workspaces/0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90/billing" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "workspaceId": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
  "enabled": true,
  "role": "owner",
  "canManage": true,
  "plan": "free",
  "subscribedPlan": "free",
  "inGrace": false,
  "graceEndsAt": null,
  "lapsed": false,
  "subscription": null,
  "limits": {
    "documents": 3,
    "members": 1,
    "storageBytes": 52428800,
    "versionRetentionDays": 30,
    "apiCallsPerDay": 100
  },
  "usage": {
    "documents": 1,
    "members": 1,
    "storageBytes": 257,
    "apiCallsToday": 12
  },
  "overLimits": [],
  "readOnly": false,
  "pricingUrl": "https://app.ollav.app/pricing"
}

Account

Who the API key belongs to.

Who am I

GET /api/me

The user the API key belongs to and the workspace it acts in. A cheap way to check that a key works.

Responses

StatusMeaning
200The caller.
400BadRequest
401Unauthorized
402PlanLimit
429RateLimited
500InternalError
curl "https://app.ollav.app/api/me" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "user": {
    "id": "e970d2fd-66e4-4fa0-bac2-144f81995e32",
    "email": "ada@example.com",
    "name": "Ada"
  },
  "workspace": {
    "id": "0f3c2a8e-1b7d-4c1e-9a52-6d1f0e4b7a90",
    "name": "Acme",
    "role": "owner"
  },
  "livemode": true
}

API keys

Keys are created and revoked in the web UI (https://app.ollav.app/keys). The API lists them by prefix only; a key's secret is shown once, at creation.

List API keys

GET /api/keys

The caller's API keys in the current workspace, by prefix only (a key is shown in full once, when created). Create and revoke keys at https://app.ollav.app/keys.

Responses

StatusMeaning
200Keys, newest first.
400BadRequest
401Unauthorized
402PlanLimit
429RateLimited
500InternalError
curl "https://app.ollav.app/api/keys" \
  -H "Authorization: Bearer $OLLAV_API_KEY"

Response (200):

{
  "keys": [
    {
      "id": "00d97554-4eb5-4d98-b0da-b88d78d6f88e",
      "keyPrefix": "ol_live_mSos",
      "name": "Claude Code",
      "scopes": [
        "upload",
        "read",
        "comment"
      ],
      "createdAt": 1793107356443,
      "lastUsedAt": 1793107360365,
      "revokedAt": null,
      "apiVersion": "2026-11-01",
      "mode": "live"
    }
  ]
}

Revoke an API key

DELETE /api/keys/{id}

Revokes one of the caller's API keys in the current workspace, at once and for good; requests with it then answer 401. A key can revoke itself, e.g. when an agent is done with it. Unknown ids and other people's keys get the same 404. Creating keys needs the web UI (https://app.ollav.app/keys).

Parameters

NameInTypeRequiredDescription
idpathstringyesThe key's id, from GET /api/keys.

Responses

curl -X DELETE "https://app.ollav.app/api/keys/00d97554-4eb5-4d98-b0da-b88d78d6f88e" \
  -H "Authorization: Bearer $OLLAV_API_KEY" \
  -H "X-Ollav-Client: agent"

Response (200):

{
  "ok": true
}

Error responses

The error responses operations share. Each is the JSON error object described in Basics; match on code, and see the errors page for every code's cause and fix.

BadRequest

The request is invalid; error.param names the field and error.hint the fix. Codes: BAD_JSON, BAD_REQUEST, VALIDATION_ERROR, MISSING_HTML, MISSING_FILE, MISSING_SLUG, EMPTY_BODY, EMPTY, NOT_HTML, INVALID_VISIBILITY, UNSUPPORTED_TYPE, UNSUPPORTED_CONTENT_TYPE, TOO_MANY_REACTIONS, CONFIRMATION_REQUIRED, INVALID_API_VERSION (an unknown Ollav-Version) and IDEMPOTENCY_KEY_INVALID.

Unauthorized

No valid API key (AUTH_REQUIRED). The key is missing, wrong or revoked, or its user left the key's workspace.

PlanLimit

The workspace's plan doesn't allow this (PLAN_LIMIT), it is over its limits and read-only (PLAN_READ_ONLY), or the key's workspace used its daily API calls. Tell the user; don't retry.

Forbidden

Not allowed: the key lacks a scope (SCOPE_REQUIRED), the X-Ollav-Client header is missing (CSRF_REQUIRED), a browser Origin is foreign (ORIGIN_FORBIDDEN), the caller's role is too low (ROLE_REQUIRED), or the action needs a browser session (FORBIDDEN).

NotFound

Not found (NOT_FOUND), or archived (ARCHIVED). Missing, forbidden and other-workspace resources all return the identical 404 (apart from request_id).

Conflict

Clashes with the current state (CONFLICT, e.g. the comment already has an image), or an Idempotency-Key problem: reused for a different request (IDEMPOTENCY_KEY_REUSED) or still in use by a running request (IDEMPOTENCY_KEY_IN_USE).

PayloadTooLarge

Over the size limit (TOO_LARGE): 15 MB per HTML document, 5 MB per image.

RateLimited

Too many requests (RATE_LIMITED): over 30 publishes or 120 comments an hour per account (looser per IP), too many exports, or a test key (ol_test_) over its 60 requests a minute. error.retry_after and the Retry-After header say how many seconds to wait; don't retry in a loop.

Gone

The document was taken down for breaking the Terms of Service (DOCUMENT_REMOVED). Don't retry.

InternalError

Something failed on our side (INTERNAL_ERROR, type api_error). Retry once with the same Idempotency-Key; if it persists, report error.request_id.

Machine-readable