Skip to content

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.com

The 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-token

The session supplies the author. Do not send an author field. Unknown JSON fields are refused.

Endpoint index

MethodPathSessionResponse
GET/v1/confignoProject configuration
GET/v1/eventsyes, workspace memberStream of theme changes
POST/v1/sessionsnoGuest session
POST/v1/sessions/googlenoSession from a Google ID token
POST/v1/sessions/tokennoSession from your identity token
POST/v1/sessions/renewyesNew session for the same person
GET/v1/threadsnoThreads for a page or the site
POST/v1/threadsyesNew thread
GET/v1/threads/{threadId}optionalThread with its comments
POST/v1/threads/{threadId}/repliesyesNew reply
PATCH/v1/threads/{threadId}/statusyesUpdated thread
DELETE/v1/threads/{threadId}/bodies/{bodyId}yes{ "threadDeleted": bool }
POST/v1/threads/{threadId}/bodies/{bodyId}/reactionsyesCurrent reactions
POST/v1/threads/{threadId}/mentions/{mentionId}/stopyesMention with stopping: true
POST/v1/uploadsyesUpload ticket
GET/v1/threads/{threadId}/attachments/{attachmentId}noSigned 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: theme
data: "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
}
  • expiresAt and renewAfter are Unix seconds. renewAfter is 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/token lasts 12 hours or the project’s session length, whichever is shorter. It is never renewed.
  • A signed-in session’s author also has email. Comment authors never do.
  • canManageWork says the person is a workspace member. The server checks membership again on every member-only operation. Signing in never creates membership.
EndpointBody
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/renewNo 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=/pricing
GET /v1/threads?site=true

Use 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 requestA retry with the same requestId gets
Was saved201 with what it created. Nothing is posted or notified again.
Is still being saved409 request_in_progress with Retry-After in seconds.
FailedProcessed as a new request.
Stopped mid-request on the server409 request_in_progress for up to five minutes, then processed as new.
Was saved, and the comment was deleted since404 not_found. Not posted again.
  • A requestId belongs 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 requestId used on a new thread cannot be used on a reply, or on another thread. That returns 409 request_id_reused.

Uploads

Uploading an image takes two steps.

  1. Ask for a ticket:

    { "contentType": "image/webp", "bytes": 184320, "kind": "file" }

    The response has id, uploadUrl, headers and url.

  2. PUT the bytes to uploadUrl with 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.

MethodPath on https://mcp.gutternote.comPurpose
POST/mcpThe MCP server. Needs a bearer access token.
GET/.well-known/oauth-protected-resource/mcpWhich server issues tokens for /mcp.
GET/.well-known/oauth-authorization-serverOAuth metadata.
POST/oauth/registerDynamic client registration.
GET/oauth/authorizeStarts approval in the browser.
POST/oauth/tokenExchanges a code or a refresh token.
POST/oauth/revokeRevokes a token and ends the connection.

https://api.gutternote.com/mcp reaches the same server.

Error codes are in Errors.