Docs

One script tag on your site, a link for reviewers, and an agent that closes the threads. Everything below is copy-ready; hostnames are filled in for this deployment.

Two-minute setup

The guided setup does all of this on one screen after you sign in. By hand, it is three steps:

  1. Register your site

    In the dashboard, add the origin your pages are served from, for example https://www.example.com. You get the snippet.

  2. Paste the snippet

    Before </body> on every page you want reviewed. Nothing else changes on your site.

  3. Share a review link

    Mint one on the site page and send the URL. Reviewers pick a name once and start commenting. No accounts.

<script src="{API_BASE}/widget/v1/widget.js" data-site-key="pk_…" data-review="on" defer></script>

The site page always shows this with your real key filled in. The key is public by design: the API only accepts it from your site's origin.

What reviewers see

A reviewer opens any page of your site through the review link, types a display name once, and the page is live for comments. Try it on the demo first.

Hover, then click the bubble

Every heading, paragraph and list item shows a small bubble in the margin on hover. Clicking it opens the thread for exactly that text; the text is captured with the thread so it stays meaningful after the page changes.

Comment mode: press C

Any element gets a box on hover, buttons, images, nav items, form fields. Click one to pin a comment to it. Esc or C leaves comment mode again.

The review bar, bottom left

Comment mode switches boxing on and off. Reveal outlines every element that has a thread. The filter shows all, only mine, unread, or open threads.

Threads have a status

Threads start new, move to in review when someone picks them up, and are closed with the version that fixed them. Reviewers can reply, edit and delete their own comments.

Comments re-anchor by content when a paragraph moves between deploys. When the text is gone, the thread lands in an orphaned tray, still readable and closable.

Scripts and CI: the API token

Agents connect over MCP with a browser sign-in (next section) and never see a token. For a script, a CI job or a harness that cannot sign in, mint an API token under Tokens & billing and send it as Authorization: Bearer ak_…. It acts as you, across every site in your account, with no cookie and no Origin header.

Pointing an agent at this product? Give it {PORTAL_BASE}/llms.txt: the whole flow below in plain text. Or use Set up with my agent on the home page: you connect your harness with the token once, and the agent gets a short prompt without any secret that walks it through registering the site and pasting the snippet.

Keep the token out of the agent. Put it in the harness configuration (the MCP header, or an environment variable for scripts) and let the prompt refer to the connection. An agent that is handed a secret in chat is right to refuse it; a token in a transcript is a token you no longer control.

Register a site and get its snippet

curl -s -X POST {API_BASE}/sites \
  -H "Authorization: Bearer ak_…" -H "Content-Type: application/json" \
  -d '{"name": "Acme", "origin": "https://www.acme.example"}'

The answer carries id, the raw site_key and the ready-made snippet. The snippet is readable again any time with GET /sites/{id}.

Mint a review link

curl -s -X POST {API_BASE}/sites/<site_id>/links \
  -H "Authorization: Bearer ak_…" -H "Content-Type: application/json" \
  -d '{"label": "design partners"}'

You get share_url and the raw code, once. Revoke with DELETE /sites/{id}/links/{link_id}.

Work the threads

# open threads on the home page ("index"); other pages use their path as the slug
curl -s "{API_BASE}/api/threads?page=index" \
  -H "Authorization: Bearer ak_…" -H "X-Site-Id: <site_id>"

# reply, then close with the commit that shipped the fix
curl -s -X POST {API_BASE}/api/threads/<thread_id>/comments \
  -H "Authorization: Bearer ak_…" -H "X-Site-Id: <site_id>" \
  -H "Content-Type: application/json" -d '{"body": "Fixed in a1b2c3"}'
curl -s -X PATCH {API_BASE}/api/threads/<thread_id> \
  -H "Authorization: Bearer ak_…" -H "X-Site-Id: <site_id>" \
  -H "Content-Type: application/json" -d '{"status": "closed", "site_version": "a1b2c3"}'

Each thread carries page_slug, anchor and context_snapshot, the exact text the reviewer saw, which is how an agent finds the spot in the source.

Connect an agent over MCP

The API hosts an MCP server at {API_BASE}/mcp (streamable HTTP). Point your client at the URL: it registers itself and opens your browser to sign in the first time, then keeps a refreshable credential of its own. No token is copied anywhere.

claude mcp add --transport http comments {API_BASE}/mcp

The first time the server is used, your browser opens on the sign-in service. Claude Code keeps a refreshable credential of its own; nothing is copied anywhere.

In a session, /mcp lists the server and /comments:triage runs the triage prompt for a site.

How the sign-in works

The API is an OAuth resource server for the central sign-in service. It serves {API_BASE}/.well-known/oauth-protected-resource/mcp, answers an unauthenticated MCP call with a WWW-Authenticate challenge that points there, and accepts the issuer's RS256 tokens for its own audience next to ak_… tokens on every owner route. Your first visit needs a verified email in the grant; after that the stable subject alone is enough. The same email as your magic-link account lands you in the same account.

Tools

ToolWhat it does
list_sites()Your sites with their enabled state and open-thread counts. Start here for a site_id.
create_site(origin, name?)Register a website. Returns the site id, the publishable key and the embed snippet.
get_site(site_id)One site with its snippet, allowed origins and open-thread count.
create_review_link(site_id, label)Mint a share link for reviewers. The URL is returned once.
list_review_links(site_id)Every link with whether it is live and how many reviewers joined through it.
list_threads(site_id, page?, include_closed?, orphaned_only?)Threads on a site, optionally for one page slug, each with its comments and captured text. orphaned_only keeps the ones whose element is gone.
get_thread(site_id, thread_id)One thread in full.
reply(site_id, thread_id, body)Add a comment as the owner.
set_status(site_id, thread_id, status, site_version?)Move a thread to new, in_review or closed; pass the deployed commit when closing.
delete_thread(site_id, thread_id)Remove one thread and its comments for good.
clear_threads(site_id, page?, status?, orphaned?, before?)Bulk delete: a page, a status, the orphans, everything older than a date, or with no filter the whole site's threads. Keys, links and reviewers stay.
export_site(site_id)Every thread and comment as one JSON document.

The triage(site_id) prompt walks the open threads: find each element in the source by its page slug and captured text, fix it, reply with one line, close with the deployed commit, and leave what it cannot resolve in in review with a reason.

Snippet options

AttributeDefaultMeaning
data-site-keyrequiredThe site's publishable key. Public by design: accepted only from the site's origin.
data-reviewonComment mode (press C, box any element) and the bottom-left review bar. Set off for paragraph bubbles only.
data-page-slugthe pathThreads are grouped under it: index for the site root, otherwise the path without .html. Set it when several URLs render the same content.
data-target-scopearticle, mainCSS selector for the containers whose headings, paragraphs and list items get bubbles.
data-versionpage hashA deploy identifier (a commit SHA works well), stored on every comment and on closed threads.
data-api-basethe script's originOnly needed if you serve the widget file yourself.

The origin must match

The widget's first call sends the page's Origin; the API answers only for the site's origin or one of its extra allowed origins. An origin is scheme, host and port with no path: https://www.example.com and https://example.com are two different sites. Add a staging host or a local dev server (http://localhost:3000) under Allowed origins on the site page.

Reviewers do not have accounts. They join through a link you mint on the site page, labelled for the cohort ("design partners", "client round 2"), optionally with an expiry.

https://your-site/?review=rv_…
  • The code works on any page of the site, as ?review=… or #review=…, so you can send reviewers straight to the page in question. The widget removes it from the address bar and remembers the reviewer in that browser.
  • Review link for me mints a link labelled owner so you comment through the same path reviewers use.
  • Revoke a link and every reviewer who joined through it is out on their next request; their comments stay. Revoke one reviewer from the reviewers table to remove a single person.
  • An expired link behaves like a revoked one. A locked-out reviewer sees a quiet "ask the site owner for a review link" note; a fresh link lets them back in.

Clearing, resetting, rebuilding

  • Delete a thread from the site page or with DELETE /api/threads/{id}. Owners only; reviewers close threads and delete their own comments.
  • Clear in bulk with DELETE /sites/{id}/threads: filter by page, status, orphaned=true or before=<datetime>, or send no filter to reset the site. Keys, review links and reviewers stay, so pages and reviewers keep working.
  • Auto-clear a playground or demo site by setting delete threads older than N days on its site page; the retention job runs a few times a day.
  • Export everything as JSON from the site page or GET /sites/{id}/export before a rebuild.
  • After a rebuild, the widget re-anchors each thread: same text means a moved paragraph and is re-attached; edited text is matched on the captured snapshot; anything else is reported as orphaned and shown in the reviewers' tray. The site page lists orphans, and Close all orphans or clear orphaned finishes the round.
  • Switch the widget off at the top of the site page to pause; rotate the site key to cut off pages that still carry an old snippet; delete the site to remove everything, which cannot be undone.

HTTP API reference

Base URL {API_BASE}. Owner routes accept the dashboard's session cookie, Authorization: Bearer ak_…, or a bearer from the central sign-in. Reviewer routes take Authorization: Bearer rt_… plus X-Site-Key and a matching Origin. Thread routes accept either; an owner names the site with X-Site-Id. Raw keys, codes and tokens are returned once. The interactive OpenAPI reference is at the API's /docs.

All routes
MethodPathPrincipalPurpose
POST/auth/magic-linknone, rate limitedsend the sign-in link; always 202
POST/auth/magic-link/verifynoneconsume the link, create a session, set the cookie
POST/auth/logoutownerrevoke the session
GET/accountowneruser, account, plan
GET / POST/account/tokensownerlist, mint (ak_ raw once)
DELETE/account/tokens/{id}ownerrevoke
GET / POST/sitesownerlist, create (pk_ raw once and the snippet)
GET / PATCH / DELETE/sites/{id}ownerdetail with snippet, {name, enabled, allowed_origins, retention_days}, delete
POST/sites/{id}/keys/rotateownernew pk_, old revoked
GET / DELETE/sites/{id}/threads?page=&status=&orphaned=&before=ownerthreads across the site, filtered; delete them in bulk (no filter: all)
GET/sites/{id}/exportownerevery thread and comment as one JSON download
GET / POST/sites/{id}/linksownerlist, mint (rv_ raw once and the share URL)
DELETE/sites/{id}/links/{lid}ownerrevoke the link and its reviewers' access
GET/sites/{id}/reviewersownerwho joined through which link
DELETE/sites/{id}/reviewers/{rid}ownerrevoke one reviewer
GET/widget/config?site_key=Origin + key{site_id, name, enabled}; the widget's first call
POST/widget/joinOrigin + X-Site-Key, rate limited{code, display_name}{token, reviewer_id, display_name, site_id}
GET/api/threads?page=reviewer or ownerthreads with comments for a page slug
POST/api/threadsreviewer or ownernew thread with its first comment
PATCH/api/threads/{id}reviewer or ownerre-anchor, status, resolved, orphaned
DELETE/api/threads/{id}ownerdelete the thread and its comments
POST/api/threads/{id}/commentsreviewer or ownerreply
PATCH / DELETE/api/threads/{id}/comments/{cid}same authoredit, soft delete
GET/api/unread?pages=reviewer or owneropen-thread counts per page slug (comma separated)
POST/billing/checkoutownerStripe Checkout URL; 503 when billing is not configured
POST/billing/portalownerStripe customer portal URL; 503 when billing is not configured
POST/billing/webhookStripe signatureplan state
GET/healthz, /readyznoneliveness, database ping
GET/widget/v1/widget.js, /widget/v1/widget.cssnonethe widget; browsers re-check it every ten minutes
*/mcpak_ or sign-in bearerthe MCP endpoint (streamable HTTP)
GET/.well-known/oauth-protected-resource[/mcp]noneOAuth resource metadata (RFC 9728); 404 when no issuer is configured

Limits: 4,000 characters per comment, 500 threads per page, 200 comments per thread, 10 extra origins per site. Rate limits answer 429 with a Retry-After header. A resource from another account is a 404, never a 403.