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:
-
Register your site
In the dashboard, add the origin your pages are served from, for example
https://www.example.com. You get the snippet. -
Paste the snippet
Before
</body>on every page you want reviewed. Nothing else changes on your site. -
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.
Add to .cursor/mcp.json in the project, or the global one in your home directory. Without a header Cursor prompts you to sign in through the browser:
{
"mcpServers": {
"comments": {
"url": "{API_BASE}/mcp"
}
}
}
Claude Desktop goes through a local bridge such as the mcp-remote package (a
third-party tool: install and check it yourself before using it). The bridge runs the browser
sign-in for you. In claude_desktop_config.json:
{
"mcpServers": {
"comments": {
"command": "npx",
"args": ["-y", "mcp-remote", "{API_BASE}/mcp"]
}
}
}
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
| Tool | What 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
| Attribute | Default | Meaning |
|---|---|---|
data-site-key | required | The site's publishable key. Public by design: accepted only from the site's origin. |
data-review | on | Comment mode (press C, box any element) and the bottom-left review bar. Set off for paragraph bubbles only. |
data-page-slug | the path | Threads 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-scope | article, main | CSS selector for the containers whose headings, paragraphs and list items get bubbles. |
data-version | page hash | A deploy identifier (a commit SHA works well), stored on every comment and on closed threads. |
data-api-base | the script's origin | Only 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.
Review links
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
ownerso 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 bypage,status,orphaned=trueorbefore=<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}/exportbefore 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
| Method | Path | Principal | Purpose |
|---|---|---|---|
| POST | /auth/magic-link | none, rate limited | send the sign-in link; always 202 |
| POST | /auth/magic-link/verify | none | consume the link, create a session, set the cookie |
| POST | /auth/logout | owner | revoke the session |
| GET | /account | owner | user, account, plan |
| GET / POST | /account/tokens | owner | list, mint (ak_ raw once) |
| DELETE | /account/tokens/{id} | owner | revoke |
| GET / POST | /sites | owner | list, create (pk_ raw once and the snippet) |
| GET / PATCH / DELETE | /sites/{id} | owner | detail with snippet, {name, enabled, allowed_origins, retention_days}, delete |
| POST | /sites/{id}/keys/rotate | owner | new pk_, old revoked |
| GET / DELETE | /sites/{id}/threads?page=&status=&orphaned=&before= | owner | threads across the site, filtered; delete them in bulk (no filter: all) |
| GET | /sites/{id}/export | owner | every thread and comment as one JSON download |
| GET / POST | /sites/{id}/links | owner | list, mint (rv_ raw once and the share URL) |
| DELETE | /sites/{id}/links/{lid} | owner | revoke the link and its reviewers' access |
| GET | /sites/{id}/reviewers | owner | who joined through which link |
| DELETE | /sites/{id}/reviewers/{rid} | owner | revoke one reviewer |
| GET | /widget/config?site_key= | Origin + key | {site_id, name, enabled}; the widget's first call |
| POST | /widget/join | Origin + X-Site-Key, rate limited | {code, display_name} → {token, reviewer_id, display_name, site_id} |
| GET | /api/threads?page= | reviewer or owner | threads with comments for a page slug |
| POST | /api/threads | reviewer or owner | new thread with its first comment |
| PATCH | /api/threads/{id} | reviewer or owner | re-anchor, status, resolved, orphaned |
| DELETE | /api/threads/{id} | owner | delete the thread and its comments |
| POST | /api/threads/{id}/comments | reviewer or owner | reply |
| PATCH / DELETE | /api/threads/{id}/comments/{cid} | same author | edit, soft delete |
| GET | /api/unread?pages= | reviewer or owner | open-thread counts per page slug (comma separated) |
| POST | /billing/checkout | owner | Stripe Checkout URL; 503 when billing is not configured |
| POST | /billing/portal | owner | Stripe customer portal URL; 503 when billing is not configured |
| POST | /billing/webhook | Stripe signature | plan state |
| GET | /healthz, /readyz | none | liveness, database ping |
| GET | /widget/v1/widget.js, /widget/v1/widget.css | none | the widget; browsers re-check it every ten minutes |
| * | /mcp | ak_ or sign-in bearer | the MCP endpoint (streamable HTTP) |
| GET | /.well-known/oauth-protected-resource[/mcp] | none | OAuth 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.