HTTP API
Base URL: https://api.gutternote.com
Request headers
Every /v1 request needs the project key and an allowed browser origin:
Gutternote-Key: pk_live_...Origin: https://preview.acme.comThe key identifies the project. The origin authorizes the site. A refused
origin gets 403 origin_not_allowed and no CORS permission.
The one origin admitted for every project, whatever its sites, is the Gutternote
browser extension’s own (chrome-extension://clmhlocgchakdiffegfmhkejbdddbemg).
A page cannot send it, so a site still has to be registered to use the key from
its own pages.
Writes also need a session from one of the session endpoints:
Gutternote-Session: session-tokenThe session supplies the author. Do not send an author field. Unknown JSON
fields are refused.
Endpoint index
| Method | Path | Session | Response |
|---|---|---|---|
GET | /v1/config | no | Project configuration |
GET | /v1/events | yes, workspace member | Stream of theme changes |
POST | /v1/sessions | no | Guest session |
POST | /v1/sessions/google | no | Session from a Google ID token |
POST | /v1/sessions/token | no | Session from your identity token |
POST | /v1/sessions/renew | yes | New session for the same person |
GET | /v1/threads | no | Threads for a page or the site |
POST | /v1/threads | yes | New thread |
GET | /v1/threads/{threadId} | optional | Thread with its comments |
POST | /v1/threads/{threadId}/replies | yes | New reply |
PATCH | /v1/threads/{threadId}/status | yes | Updated thread |
DELETE | /v1/threads/{threadId}/bodies/{bodyId} | yes | { "threadDeleted": bool } |
POST | /v1/threads/{threadId}/bodies/{bodyId}/reactions | yes | Current reactions |
POST | /v1/threads/{threadId}/mentions/{mentionId}/stop | yes | Mention with stopping: true |
POST | /v1/uploads | yes | Upload ticket |
GET | /v1/threads/{threadId}/attachments/{attachmentId} | no | Signed view URL |
Configuration
GET /v1/config returns what the widget needs to draw itself:
{ "projectId": "...", "name": "Acme Website", "backend": "postgres", "capabilities": { "attachments": true, "edit": true, "delete": true }, "auth": { "access": { "commentAccess": "anyone", "allowsGuests": true } }, "agents": [], "agentTarget": "new_pull_request", "hidden": false, "theme": "", "screenshots": true, "uploads": { "maxBytes": 8388608, "contentTypes": ["image/jpeg", "image/png", "image/webp"] }}theme is the theme picked under Configure → Theme. An empty string is
Classic.
Theme changes
GET /v1/events is a text/event-stream for a session that belongs to a
member of the project’s workspace. Any other session gets 403. The first
event is the current theme, and each change follows:
event: themedata: "space"The server closes the stream after a few minutes. Open a new one.
hidden is true when the origin is switched off under Where the widget
shows. Fields an older server does not send mean “no”.
Sessions
The three sign-in endpoints return 201. Renew returns 200. All four return:
{ "token": "session-token", "author": { "id": "...", "kind": "guest", "displayName": "Dana Reed" }, "expiresAt": 1790034900, "renewAfter": 1789732500, "canManageWork": false}expiresAtandrenewAfterare Unix seconds.renewAfteris halfway through the session.- A guest or Google session lasts the project’s session length (7 days by default).
- A session from
POST /v1/sessions/tokenlasts 12 hours or the project’s session length, whichever is shorter. It is never renewed. - A signed-in session’s
authoralso hasemail. Comment authors never do. canManageWorksays the person is a workspace member. The server checks membership again on every member-only operation. Signing in never creates membership.
| Endpoint | Body |
|---|---|
POST /v1/sessions | { "displayName": "Dana Reed" }, or empty. Refused unless the project allows guests. |
POST /v1/sessions/google | { "credential": "google-id-token" } |
POST /v1/sessions/token | { "token": "customer-signed-jwt" }. See Identity tokens. |
POST /v1/sessions/renew | No body. Send the current session header. |
POST /v1/sessions/extension is not for other clients. It is how the Gutternote
browser extension signs a workspace member in on a page that already carries the
widget: it accepts only the extension’s own origin and a bearer token issued to
the extension, and answers 403 extension_only or 403 wrong_client to
anything else.
Renew checks the project’s rules again. If they no longer admit the person, or
the session expired, it returns 401 or 403. Drop the session and sign in
again.
Threads
List threads
GET /v1/threads?path=/pricingGET /v1/threads?site=trueUse one scope. path returns the anchors for one route. site=true returns
every route with enough text for the site-wide list. Optional filters:
deploymentId (UUID), prNumber, and includeResolved=true. Archived threads
are never returned.
Create a thread
{ "anchor": { "path": "/pricing", "strategy": "selector", "selector": "[data-gutternote-id=\"cta\"]", "textQuote": "Start free trial", "x": 0.5, "y": 0.5, "viewportW": 1440, "viewportH": 900 }, "deployment": { "url": "https://preview.acme.com", "branch": "fix/nav", "commitSha": "3ab19f2", "prNumber": 42 }, "markdown": "The CTA is four pixels off.", "attachments": [], "mentions": [], "requestId": "0b6b3e0e-8d1e-4c5f-9a43-0d6f5f1f2a77"}x and y are fractions of the element’s box. The anchor can also carry
pageX and pageY (fractions of the document), region (a dragged box), and
sourceLoc from the Vite plugin. markdown may be
empty when there is an attachment. Returns 201 with the thread.
Reply
POST /v1/threads/{threadId}/replies takes markdown, attachments,
mentions and an optional requestId. Returns 201 with the new comment.
A comment’s author has id, kind, displayName and sometimes avatarUrl.
It never has an email. To find your own comments, compare author.id with the
session’s author.id.
Change status
{ "status": "resolved" }PATCH /v1/threads/{threadId}/status accepts open, resolved or
archived. Returns the updated thread.
Delete a comment
A commenter can delete only their own comment. Deleting the last comment also deletes the thread.
Retry a new thread or reply
A response can be lost after the server saved the comment. A plain retry then
posts it twice. To make a retry safe, create one UUID per comment and send it
as requestId on the first attempt and on every retry.
| If the first request | A retry with the same requestId gets |
|---|---|
| Was saved | 201 with what it created. Nothing is posted or notified again. |
| Is still being saved | 409 request_in_progress with Retry-After in seconds. |
| Failed | Processed as a new request. |
| Stopped mid-request on the server | 409 request_in_progress for up to five minutes, then processed as new. |
| Was saved, and the comment was deleted since | 404 not_found. Not posted again. |
- A
requestIdbelongs to the session’s author. A retry with a new guest session counts as a different person. - The server does not compare bodies. Give each different comment a new
requestId. - A
requestIdused on a new thread cannot be used on a reply, or on another thread. That returns409 request_id_reused.
Uploads
Uploading an image takes two steps.
-
Ask for a ticket:
{ "contentType": "image/webp", "bytes": 184320, "kind": "file" }The response has
id,uploadUrl,headersandurl. -
PUTthe bytes touploadUrlwith every returned header. Send no Gutternote credentials.
Then put id, url, contentType, bytes and kind in the thread’s or
reply’s attachments. kind is file, capture or captureCrop. Limits are
in Limits.
View an attachment
A stored attachment URL is private. GET /v1/threads/{threadId}/attachments/{attachmentId} returns a short-lived URL:
{ "url": "https://storage.googleapis.com/...?signature=...", "contentType": "image/webp", "expiresAt": "2026-09-22T12:30:00Z"}OAuth and MCP
Coding agents do not use /v1. They connect to the MCP server with OAuth. See
MCP server.
| Method | Path on https://mcp.gutternote.com | Purpose |
|---|---|---|
POST | /mcp | The MCP server. Needs a bearer access token. |
GET | /.well-known/oauth-protected-resource/mcp | Which server issues tokens for /mcp. |
GET | /.well-known/oauth-authorization-server | OAuth metadata. |
POST | /oauth/register | Dynamic client registration. |
GET | /oauth/authorize | Starts approval in the browser. |
POST | /oauth/token | Exchanges a code or a refresh token. |
POST | /oauth/revoke | Revokes a token and ends the connection. |
https://api.gutternote.com/mcp reaches the same server.
Error codes are in Errors.