Docs
1. Install the widget
Create a site in the dashboard with the exact origin your pages are
served from. You get a publishable site key (pk_…) and this snippet; paste it
before </body> on every page you want reviewed:
<script src="{API_BASE}/widget/v1/widget.js" data-site-key="pk_…" defer></script>
The site page in the dashboard always shows the snippet with your real key filled in.
Attributes
| Attribute | Required | Meaning |
|---|---|---|
data-site-key | yes | The site's publishable key. It is public by design: the API only accepts it from the site's origin. |
data-api-base | no | Where the widget talks to. Defaults to the origin the script was loaded from, which is the API, so you normally leave it out. |
data-page-slug | no | The identifier threads are grouped under. Default: index for a path ending in /, otherwise the last path segment without .html. Set it when several URLs render the same content, or when a page's path changes. |
data-target-scope | no | CSS selector for the containers whose headings, paragraphs and list items become commentable. Default: article, main. |
data-version | no | A deploy identifier (a commit SHA works well). It is stored on every comment and on a thread when it is closed, so you can tell which version a comment was about. |
The origin must match
The widget's first call is GET /widget/config with the page's Origin.
The API answers only when that origin is the site's origin or one of its extra allowed origins;
any other page gets 403 origin does not match site and the widget stays silent.
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.
2. Review links
Reviewers do not have accounts. They join a site through a review link you mint and share.
Minting one
- Open the site page in the dashboard and go to Review links.
- Give it a label that names the cohort ("design partners", "internal", "client round 2") and, optionally, an expiry.
- The share URL and the raw code (
rv_…) are shown once. Copy the share URL and send it on.
Use Review link for me to mint a link labelled owner for yourself: owners
comment through the same path reviewers use, so there is exactly one identity path in the widget.
An MCP client can also mint links with create_review_link (section 4).
Sharing it
The share URL is the site's origin with the code as a query parameter:
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 you want looked at.
What reviewers see
- The widget removes the code from the address bar and asks for a display name, once.
- It exchanges the code for a reviewer token that lives in that browser's local storage, per site. A different browser or device is a new reviewer.
- Hovering a heading, paragraph or list item shows a comment handle; clicking it opens the thread for that element, with the exact text captured as the thread's context.
- Reviewers can reply to any thread, edit and delete their own comments, and move threads between new, in review and closed.
Revoking
- Revoke a link and every reviewer who joined through it loses access on their next request. Their comments remain.
- Revoke one reviewer from the Reviewers table to remove a single person without disturbing the rest of the cohort.
- An expired link behaves like a revoked one: it admits nobody new and its existing reviewers are locked out too.
A locked-out reviewer sees a quiet "ask the site owner for a review link" note in the widget; a fresh link from you lets them join again.
3. Turning the widget off
Every site has an on/off switch at the top of its page in the dashboard. When it is off,
GET /widget/config reports enabled: false, the widget attaches nothing
and exits. Pages honour the change on their next load, with no deploy on your side.
- Threads, comments, links and reviewers are all kept. Switching back on brings everything back.
- Reviewer tokens stay valid but are refused while the site is off, so nobody can read or write in the meantime.
- To cut off the pages themselves rather than the reviewers, rotate the site key: the old
pk_…is revoked at once and pages still carrying the old snippet get401until they are updated to the new one. - To remove everything, delete the site. That also deletes its threads and comments and cannot be undone.
4. MCP setup
The API hosts an MCP server at {API_BASE}/mcp (streamable HTTP). It
authenticates with an API token from Tokens & billing,
sent as Authorization: Bearer ak_…. The token acts as you: every site in your
account, and the same authorization the dashboard has.
Claude Code
claude mcp add --transport http comments {API_BASE}/mcp --header "Authorization: Bearer ak_…"
Then, in a session: /mcp lists the server, and /comments:triage runs the triage prompt for a site.
Cursor
Add to .cursor/mcp.json in the project (or the global one in your home directory):
{
"mcpServers": {
"comments": {
"url": "{API_BASE}/mcp",
"headers": {
"Authorization": "Bearer ak_…"
}
}
}
}
Claude Desktop
The server needs a client that speaks HTTP MCP and can send a bearer header. Claude Desktop's
connector settings take a URL but do not let you set an Authorization header, so
connect it through a local stdio-to-HTTP bridge such as the mcp-remote package
(a third-party tool: install it yourself and check it before trusting it with a token).
In claude_desktop_config.json:
{
"mcpServers": {
"comments": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"{API_BASE}/mcp",
"--header", "Authorization: Bearer ak_…"
]
}
}
}
If you would rather not run a bridge, use Claude Code or Cursor, which connect directly.
Tools
| Tool | What it does |
|---|---|
list_sites() | Your sites with their enabled state and open-thread counts. Start here to get a site_id. |
list_threads(site_id, page?, include_closed?) | Threads on a site, optionally for one page slug; open threads only unless include_closed is true. Each carries its comments and the text it was anchored to. |
get_thread(site_id, thread_id) | One thread in full. |
reply(site_id, thread_id, body) | Add a comment to a thread 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 as site_version when closing. |
create_review_link(site_id, label) | Mint a share link for reviewers. The share URL is returned once. |
The triage prompt
triage(site_id) asks the agent to list the open threads of a site and, for each
one, find the element in the site's source using page_slug, anchor and
context_snapshot (the exact text the reviewer saw), make the fix, reply
with a one-line summary, and set_status to closed with the deployed
commit as site_version. Threads it cannot resolve are left in_review
with a reply explaining why.
5. API overview
Base URL {API_BASE}. Owner routes accept the dashboard's session cookie or
Authorization: Bearer ak_…. Reviewer routes take Authorization: Bearer rt_…
plus X-Site-Key and a matching Origin. Thread routes accept either; an
owner names the site with the X-Site-Id header. Raw keys, codes and tokens are
returned once and never again. The interactive OpenAPI reference is at
the API's /docs.
| 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, {name, enabled, allowed_origins}, delete |
| POST | /sites/{id}/keys/rotate | owner | new pk_, old revoked |
| 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 |
| 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 versioned widget, cached immutably |
| * | /mcp | ak_ bearer | the MCP endpoint (streamable HTTP) |
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.