# Redpen API — Agent Reference Redpen hosts HTML documents (prototypes, articles) at stable URLs and enables element-anchored threaded comments. Humans use the web UI; agents use this REST API. ## Base URL Production: https://app.redpens.app (Served HTML lives at: https://content.redpens.app/p/{slug}) ## Authentication All API endpoints accept an API key via the Authorization header: Authorization: Bearer sk_live_ API keys have scopes: upload, read, comment. Create keys at /keys (web UI, session required). Keys are shown once at creation and stored as SHA-256 hashes. All state-changing endpoints also require: X-Redpen-Client: agent This header is the CSRF guard. Always include it. ## Endpoints ### Upload a document POST /api/documents Scopes required: upload Content-Type: application/json (or multipart/form-data, or text/html) JSON body: { "html": "...", "title": "My Doc" } visibility: "authed" (default) — sign-in required; allowed accounts or invited people only "link" — PUBLIC: anyone with the URL, no sign-in, INCLUDING people outside the company. The slug is unguessable, but there is no access check. Only use for deliberate external sharing. Response 201: { "url": "https://content.redpens.app/p/{slug}", "slug": "...", "documentId": "...", "versionId": "...", "hash": "...", "deduped": false, "versioned": false } Example (small inline document): curl -X POST https://app.redpens.app/api/documents \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d '{"html":"Hello","title":"Test"}' Example (a file on disk — ALWAYS use this for built apps, see "Uploading a built app" below; curl reads the file, so an agent spends ZERO tokens on it): curl -X POST https://app.redpens.app/api/documents \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" \ -F "file=@dist/index.html" ### List your documents GET /api/documents Scopes required: read Response 200: { "documents": [...] } (non-archived docs, newest first) Example: curl https://app.redpens.app/api/documents \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" ### Get a single document GET /api/documents/{slug} Scopes required: read Response 200: document object with versions array ### Add a new version POST /api/documents/{slug}/versions Scopes required: upload Content-Type: application/json Body: { "html": "...", "title": "optional" } Response 201: { "slug": "...", "versionId": "...", "hash": "...", "deduped": false } Example (file on disk — preferred when iterating on a build): curl -X POST https://app.redpens.app/api/documents/{slug}/versions \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" \ -F "file=@dist/index.html" Example (small inline document): curl -X POST https://app.redpens.app/api/documents/{slug}/versions \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d '{"html":"Updated"}' ### List versions GET /api/documents/{slug}/versions Scopes required: read Response 200: { "document": {...}, "versions": [...] } ### Post a comment POST /api/comments Scopes required: comment Content-Type: application/json Body (top-level): { "slug": "...", "body": "Great prototype!", "selector": "h1", "rect": {"x":0,"y":0,"w":100,"h":40}, "elementText": "Page title", "reproduceNote": "Click 'Launch simulation' first", // optional, ≤300 chars "breadcrumbs": [{"selector":"button#launch","text":"Launch simulation"}] // optional, ≤8 items } Body (reply): { "slug": "...", "body": "Agreed!", "parentId": "" } reproduceNote: optional hint for reviewers on how to reproduce the UI state. breadcrumbs: optional array of {selector, text} interaction steps. Top-level only. Both fields are silently ignored for replies. Response 201: comment object (includes reproduceNote, breadcrumbs parsed) Example (top-level with reproduce info): curl -X POST https://app.redpens.app/api/comments \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d '{"slug":"abc123","body":"Looks good","selector":"body","rect":{"x":0,"y":0,"w":1,"h":1},"reproduceNote":"Open modal first"}' ### Attach an image to a comment POST /api/comments/{id}/attachments Scopes required: comment Content-Type: multipart/form-data (field 'file') or a raw image body One image per TOP-LEVEL comment (replies cannot have attachments). Image type is validated by magic bytes (png/jpeg/webp/gif — SVG rejected); max 5 MB. Optional 'width'/'height' form fields are layout hints. Stored content-addressed in R2 and served from the content origin, gated by the parent document's visibility. Response 201: { "id": "...", "url": "https://content.redpens.app/a/{id}[?vt=…]", "mime": "image/png", "width": 1024, "height": 768, "size": 20481 } For authed docs the url carries a short-lived ?vt token; for link docs it is open. ### List comments GET /api/comments?slug={slug} Scopes required: read Each top-level comment includes "attachment": {url, mime, width, height} | null. Response 200: { "slug": "...", "currentVersionId": "...", "count": N, "unresolvedCount": N, "comments": [ { ...comment, "reproduceNote": "..." | null, "breadcrumbs": [{selector,text}] | null, "replies": [...] } ] } ### Read feedback for iteration (agent loop) GET /api/review?slug={slug} Scopes required: read An agent-shaped digest of feedback, leaner and more actionable than GET /api/comments. Query params: status open (default) | resolved | all since ISO 8601 or epoch-ms; return only threads with NEW activity after this. Pass the response's "cursor" back here next time for idempotent polling. Response 200: { "slug": "...", "title": "...", "url": "https://content.redpens.app/p/{slug}", "currentVersionId": "...", "filter": {"status":"open","since":null}, "totalThreads": N, "openThreads": N, "returned": N, "cursor": "" | null, // pass as ?since= next call "generatedAt": "", "note": "anchor.selector is rendered DOM; prefer quote/reproduceNote for source", "threads": [ { "id": "...", "status": "open", "onCurrentVersion": true, "versionId": "...", "author": {"email":"...","name":"..."}, "createdAt": "", "lastActivityAt": "", "comment": "the feedback text", "attachment": {"url":"…/a/{id}","mime":"image/png","width":N,"height":N} | null, "anchor": { "quote": "", "reproduceNote": "..."|null, "breadcrumbs": [...]|null, "selector": "" }, "replies": [ {"author":{...},"body":"...","createdAt":""} ] } ] } onCurrentVersion:false means the thread was placed on an earlier build — its anchor may no longer exist in the current version. ### Resolve a comment POST /api/comments/{id}/resolve Scopes required: comment ### Unresolve a comment POST /api/comments/{id}/unresolve Scopes required: comment ### Delete a comment DELETE /api/comments/{id} Scopes required: comment Allowed for the comment's own author or the document owner. Top-level comments also delete all their replies. Returns { "deleted": true } on success. Returns 404 for unknown id or unauthorized callers (no existence leak). ### Dashboard (aggregate) GET /api/dashboard Scopes required: read Response 200: { "documents": [ { ...doc, versionCount, commentCount, unresolvedCount } ] } ### Archive a document POST /api/documents/{slug}/archive Scopes required: upload Archived documents return HTTP 410. Reversible via /unarchive. ### Unarchive a document POST /api/documents/{slug}/unarchive Scopes required: upload ### Update visibility PATCH /api/documents/{slug} Scopes required: upload Body: { "visibility": "authed" | "link" } Switching to "link" makes the document PUBLIC — anyone with the URL, no sign-in, including people outside the company. Do this only when the person you are working for has asked for external sharing, and tell them you did. Example (deliberate external share): curl -X PATCH https://app.redpens.app/api/documents/{slug} \ -H "Authorization: Bearer sk_live_..." \ -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d '{"visibility":"link"}' ### Set/extend expiry POST /api/documents/{slug}/expiry Scopes required: upload Body: { "action": "extend" } — adds 90 days from now { "action": "permanent" } — removes expiry { "action": "custom", "expiresAt": } ### Export document data GET /api/documents/{slug}/export Scopes required: read Returns full JSON bundle: document metadata + all versions (HTML inlined) + all comments/threads. ## UPLOADING A BUILT APP (React/Vue/etc.) — read this before generating HTML A bundled single-file app is megabytes. HOW it reaches Redpen decides whether that costs nothing or costs a fortune: BAD agent regenerates/pastes the bundle as an "html" string (JSON body, or the MCP upload_document tool). Tool args are MODEL OUTPUT — every character is generated tokens. A 6MB build is slow, expensive, and frequently exceeds the model's per-response output limit, so it cannot be produced at all. GOOD the BUNDLER writes one self-contained file to disk, then curl uploads that file. curl reads it from disk, so the agent spends ZERO tokens on content. # 1. Build one self-contained file (any bundler: vite-plugin-singlefile, webpack, …) npm run build # -> dist/index.html with CSS/JS/assets inlined # 2. Upload the FILE (new document) curl -X POST https://app.redpens.app/api/documents \ -H "Authorization: Bearer $REDPEN_API_KEY" -H "X-Redpen-Client: agent" \ -F "file=@dist/index.html" # 3. Iterating? New version at the SAME url, comments preserved curl -X POST https://app.redpens.app/api/documents/$SLUG/versions \ -H "Authorization: Bearer $REDPEN_API_KEY" -H "X-Redpen-Client: agent" \ -F "file=@dist/index.html" Raw-body form works too: -H "Content-Type: text/html" --data-binary @dist/index.html EXCEPTION — no shell or no network egress (claude.ai / Cowork sandboxes): the MCP tools are the only channel there, so a large build genuinely cannot be uploaded by the agent. Build it, then upload the file by hand at https://app.redpens.app. ## LIMITATIONS - One self-contained HTML file per upload, maximum 15 MB. - Relative asset paths (src="assets/app.js", href="styles.css", src="img/logo.png") WILL NOT load — there is no multi-file hosting. Use absolute URLs (https://…), data: URIs, or inline the assets. Tools like vite-plugin-singlefile can bundle a project into one inlined file. - No server-side code — static serving only. Client-side JS calling external CORS-friendly APIs works fine. - SPA history routing: /p/{slug}/* sub-paths all serve the same document, so deep links and reloads work. The app's BrowserRouter determines whether the exact deep route is restored (set basename appropriately in your Vite app). - The upload and new_version endpoints return an advisory warnings[] array in the response body when relative asset references or a tag are detected. Uploads are never blocked — warnings are informational only. ## Error shape { "error": { "code": "ERROR_CODE", "message": "Human-readable message" } } Common codes: AUTH_REQUIRED (401), SCOPE_REQUIRED (403), NOT_FOUND (404), CSRF_REQUIRED (403), VALIDATION_ERROR (400), TOO_LARGE (413) ## Quickstart (agent headless flow) # 1. Upload SLUG=$(curl -s -X POST https://app.redpens.app/api/documents \ -H "Authorization: Bearer $KEY" -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d '{"html":"v1"}' | jq -r .slug) # 2. Comment curl -X POST https://app.redpens.app/api/comments \ -H "Authorization: Bearer $KEY" -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d "{"slug":"$SLUG","body":"Feedback here","selector":"body","rect":{"x":0,"y":0,"w":1,"h":1}}" # 3. New version curl -X POST https://app.redpens.app/api/documents/$SLUG/versions \ -H "Authorization: Bearer $KEY" -H "X-Redpen-Client: agent" \ -H "Content-Type: application/json" \ -d '{"html":"v2"}' # 4. Read feedback curl https://app.redpens.app/api/comments?slug=$SLUG \ -H "Authorization: Bearer $KEY" -H "X-Redpen-Client: agent" ## Feedback loop (iterate on comments) The intended cycle for acting on human feedback with an agent: 1. READ GET /api/review?slug=$SLUG → open threads + a "cursor" 2. EDIT (regenerate the HTML from the feedback; use anchor.quote / anchor.reproduceNote to find each element in your SOURCE — the selector is rendered-DOM, not JSX) 3. UPDATE POST /api/documents/$SLUG/versions → new build at the SAME url 4. CLOSE POST /api/comments/$ID/resolve (optionally POST /api/comments with parentId to reply "fixed in v3 — …") 5. POLL GET /api/review?slug=$SLUG&since=$CURSOR → only NEW activity Because `since` is the high-water cursor from the previous call, step 5 is idempotent — re-running it returns nothing until a new comment or reply lands. This maps cleanly onto a polling driver (e.g. a scheduled task or Claude Code's /loop): re-run get_review on an interval, and only act when `returned` > 0. No always-on server is required — the agent is woken or polls, it does not sit on a socket. (A push/webhook trigger is not offered yet; polling covers it.) # Read only what changed since your last build curl "https://app.redpens.app/api/review?slug=$SLUG&since=$CURSOR" \ -H "Authorization: Bearer $KEY" -H "X-Redpen-Client: agent" ## MCP connector Redpen exposes a native MCP (Model Context Protocol) endpoint for claude.ai, Cowork sandboxes, and Claude Code — clients that cannot make direct egress POSTs but whose MCP traffic routes through Anthropic. Transport: Streamable HTTP (MCP spec 2025-03-26), stateless, JSON responses. ### claude.ai / Cowork setup Settings → Connectors → Add custom connector URL: https://app.redpens.app/mcp/sk_live_YOURKEY IMPORTANT: the URL contains your API key — treat it as a secret. Revoke it at /keys if it is ever exposed. The path-key form exists because claude.ai connectors cannot set custom HTTP headers. ### Claude Code claude mcp add --transport http redpen https://app.redpens.app/mcp \ --header "Authorization: Bearer sk_live_..." Or interactively: /mcp add ### Available MCP tools upload_document — host HTML at a stable URL; returns the shareable URL new_version — update content at the same URL (keeps old comments) list_documents — dashboard aggregate (version + comment counts) get_document — metadata + version history for one document get_comments — threaded comments with selector anchors and isCurrentVersion flags get_review — agent-shaped feedback digest (open threads, source-findable anchors, polling cursor) add_comment — pin an element-anchored comment (or reply to one) resolve_comment — resolve or unresolve a comment thread delete_comment — delete a comment and its replies (author or doc owner only)