greatfeedback.ai API reference
The /api/v1/* surface is the stable programmatic API. Use it from CI scripts, language SDKs, or any Claude Code agent that needs to read or update workspace annotations.
For dashboard-style flows, the same endpoints accept a Cognito session, so the FE composables and a third-party CLI share one contract.
Base URLs
| Stage | Base URL |
|---|---|
| prd | https://api.greatfeedback.ai |
| stg | https://stg-api.greatfeedback.ai |
| dev | https://dev-api.greatfeedback.ai |
| local | http://localhost:11001 |
Authentication
Pass the bearer token in the standard Authorization header.
API token (recommended for programmatic callers)
Authorization: Bearer gfat_<token>
Tokens are minted in the dashboard at workspace settings → API tokens, or via POST /api/workspaces/{ws}/api-tokens (OWNER role required to mint). Tokens are clamped to MEMBER or GUEST at issuance — OWNER and ADMIN are not assignable. The plaintext is shown ONCE at creation time; only the SHA-256 hash + last-4 chars persist on the row.
Cognito session
Authorization: Bearer <Cognito ID token>
The dashboard FE uses this. Same /api/v1/* endpoints work for both auth styles; route handlers don't care which one was used.
Required headers
| Header | When | Description |
|---|---|---|
Authorization |
always | Bearer gfat_* or Bearer <Cognito> |
X-GF-Workspace |
session callers | Workspace id; API tokens carry this implicitly |
Idempotency-Key |
POST writes | Optional; recommended for retried writes |
API tokens carry their workspace id internally — no X-GF-Workspace header is needed when using gfat_*.
Endpoints
Sites
| Method | Path | Min role | Notes |
|---|---|---|---|
| GET | /api/v1/sites |
GUEST | List workspace sites. Excludes mcp_token. |
| GET | /api/v1/sites/{id} |
GUEST | Site detail. Excludes mcp_token. |
Annotations
| Method | Path | Min role | Notes |
|---|---|---|---|
| GET | /api/v1/sites/{id}/annotations |
GUEST | Filters: ?status=&kind=&since=<epoch_ms>&auto_fix=. Cursor pagination. |
| GET | /api/v1/annotations/{id} |
GUEST | Detail with replies + resolutions. |
| PATCH | /api/v1/annotations/{id} |
GUEST | Body: {status?, summary?, assignee_id?}. |
| POST | /api/v1/annotations/{id}/replies |
GUEST | Body: {body, author_name?}. Author is agent for token callers. |
| GET | /api/v1/annotations/{id}/markdown |
GUEST | Agent-paste-friendly markdown blob. |
| POST | /api/v1/annotations/{id}/run-personas |
MEMBER | Body: {persona_ids: [...]}. Cost-bearing. |
GUEST tokens that try run-personas get 403 with Insufficient role.
The auto_fix=true|false filter restricts to (or excludes) submissions the original submitter authorised for unattended automated change. See § Auto-fix and resolution evidence.
Resolutions (auto-fix evidence)
| Method | Path | Min role | Notes |
|---|---|---|---|
| POST | /api/v1/annotations/{id}/resolutions/upload-url |
MEMBER | Body `{kind: "before" |
| POST | /api/v1/annotations/{id}/resolutions |
MEMBER | Body {before_key?, after_key?, diff_url?, explanation?}. At least one required. diff_url must be https://. explanation capped at 4 KB. Triggers async submitter notification. |
| GET | /api/v1/annotations/{id}/resolutions |
GUEST | List the resolutions attached to an annotation, oldest-first. |
A RESOLUTION row records what changed to address the annotation. Multiple resolutions per annotation are allowed (fix → revert → re-fix). On create, the original submitter is notified asynchronously: by Resend email if their address is on file, otherwise via a REPLY row appended to the annotation thread (visible to anonymous submitters when they revisit the page). The notification_status field on the row reports the outcome.
API tokens (workspace settings)
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/workspaces/{ws}/api-tokens |
OWNER session | Body: {name, role?, expires_at?}. Returns {token, ...} ONCE. |
| GET | /api/workspaces/{ws}/api-tokens |
session | OWNER sees all; MEMBER sees only their own. |
| DELETE | /api/workspaces/{ws}/api-tokens/{id} |
session | OWNER revokes any; MEMBER revokes their own. |
These management endpoints are session-only; you cannot mint or revoke a token via another token.
Secrets (dashboard-only)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/widget/sites/{id}/secrets |
OWNER session | Returns {widget_token, mcp_token, loader_secret_id}. |
| POST | /api/widget/sites/{id}/secrets/rotate-mcp-token |
OWNER session | Mints a fresh MCP token; old one immediately stops resolving. |
API tokens cannot read these endpoints. Tokens are clamped to MEMBER, while secrets require OWNER, so the auth check fails. The dashboard is the only place that ever reveals mcp_token or widget_token.
Personas
| Method | Path | Min role | Notes |
|---|---|---|---|
| GET | /api/personas/mine |
GUEST | Defaults + customs in the caller's workspace. Ordered: category asc, defaults before customs in a category, name asc. |
| GET | /api/personas/category/{category} |
GUEST | Same ordering, scoped to one category. |
| GET | /api/personas/{id} |
GUEST | Workspace-scoped read. |
| POST | /api/personas |
MEMBER | Create a custom persona. |
| PATCH | /api/personas/{id} |
MEMBER | Edit any persona (default or custom). |
| DELETE | /api/personas/{id} |
MEMBER | Delete any persona. |
| POST | /api/personas/reset-defaults |
MEMBER | Body {slug?} — slug=null resets all defaults; passing a slug resets that one. Returns {reset_count}. Customs untouched. |
Each new workspace is seeded with 60 default personas (is_default=true) at creation. is_built_in is no longer part of the API; the legacy GET /api/personas/built-in endpoint is removed.
MCP tools
Configure once: claude mcp add greatfeedback https://api.greatfeedback.ai/mcp/<mcp_token>.
The MCP server exposes:
list_annotations(status?, kind?, since?, auto_fix?, limit?)— discover open work. Passauto_fix=trueto pick only submissions the submitter authorised for unattended automated change.read_annotation(id)— full body, screenshot, computed styles, replies, markdown.acknowledge(id)— flip status* -> ack.reply(id, message)— post an agent reply on the thread.resolve(id, summary)— flip status* -> resolvedwith a summary.patch_annotation(id, status?, summary?, assignee_id?)— generalised write.get_resolution_upload_url(annotation_id, kind, content_type)— presign abefore/afterscreenshot upload. Returns{url, method, headers, key, expires_in_seconds}. PUT bytes tourlwithheaders, then passkeyback toattach_resolution.attach_resolution(annotation_id, before_key?, after_key?, diff_url?, explanation?)— append a RESOLUTION row recording what changed. At least one of the four fields must be present;diff_urlmust behttps://;explanationis markdown, 4 KB cap. Auto-notifies the submitter (email when an address is on file, otherwise a thread reply).list_resolutions(annotation_id)— list the resolutions already attached, oldest-first. Use this to avoid duplicating work.run_personas(id, persona_ids?)— kick off the AI persona pipeline.
Auto-fix and resolution evidence
Each annotation carries an auto_fix boolean. When true, the submitter authorised the consumer system (an agent, a CI bot, the engineering team) to apply the change without further discussion. Anonymous submitters always land false regardless of what the client sent — the server coerces.
Stakeholder tokens have a per-row auto_fix_default (never | optional_off | optional_on) that gates whether the toggle renders for them in the widget at all and what initial state it has. Manage via:
| Method | Path | Min role | Notes |
|---|---|---|---|
| POST | /api/widget/sites/{id}/stakeholders |
MEMBER | Body {name?, email?, auto_fix_default?}. |
| PATCH | /api/widget/sites/{id}/stakeholders/{token} |
MEMBER | Body {name?, email?, auto_fix_default?}. |
| GET | /api/widget/sites/{id}/stakeholders |
GUEST | List existing. |
| DELETE | /api/widget/sites/{id}/stakeholders/{token} |
MEMBER | Revoke. |
End-to-end agent flow:
TOKEN=gfat_<your token>
ANN=$(curl -sH "Authorization: Bearer $TOKEN" \
"https://api.greatfeedback.ai/api/v1/sites/<site>/annotations?auto_fix=true" \
| jq -r '.items[0].id')
# Upload screenshots (optional)
PRE=$(curl -sX POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"kind":"after","content_type":"image/png"}' \
"https://api.greatfeedback.ai/api/v1/annotations/$ANN/resolutions/upload-url")
URL=$(echo "$PRE" | jq -r .url); KEY=$(echo "$PRE" | jq -r .key)
curl -X PUT --data-binary @after.png -H "Content-Type: image/png" "$URL"
# Attach the resolution
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"after_key\":\"$KEY\",\"diff_url\":\"https://github.com/x/y/pull/42\",\"explanation\":\"Increased CTA contrast.\"}" \
"https://api.greatfeedback.ai/api/v1/annotations/$ANN/resolutions"
MCP tokens authenticate the caller as the site's MCP integration; every write tool stamps the audit log with actor_id="mcp:<site_id>" so an operator can grep MCP calls separately from human or API-token actions.
Status taxonomy
"new" | "ack" | "triaged" | "in_progress" | "resolved" | "won_t_fix" | "duplicate"
new— created via the widget; nobody's looked at it yet.ack— "we saw it"; no work yet.triaged— looked at and filed (e.g., into Linear/Jira). Distinct fromack.in_progress— work underway.resolved— done.won_t_fix— closed without action.duplicate— closed because another annotation covers it; pair with asummarylike "duplicate of".
Unknown status values 400 at request validation.
Pagination
List endpoints accept ?cursor=...&limit=.... Default limit 50, max 200.
Cursors are server-issued and HMAC-tamper-protected. A tampered cursor 400s; a missing cursor returns the first page. The cursor encodes the last row's sort key plus a signature; decoding it without the secret is rejected.
GET /api/v1/sites/{id}/annotations?limit=50
→ { "items": [...], "next_cursor": "<opaque>" }
GET /api/v1/sites/{id}/annotations?limit=50&cursor=<opaque>
→ { "items": [...], "next_cursor": "<opaque or null>" }
The last page returns "next_cursor": null.
Rate limits
| Bucket | Limit | Window |
|---|---|---|
| Per token | 600 | 60s |
| Per IP | 30 | 60s |
Both buckets must allow the request. Excess returns 429 with a reason string. Recovery happens automatically once the 60-second window rolls over.
The WAF adds an additional per-IP rule at the edge — those rejections never reach the application layer and don't appear in audit logs.
HTTP error codes
| Code | Meaning |
|---|---|
| 200 / 201 | Success |
| 400 | Bad request (validation, unknown status, tampered cursor) |
| 401 | Missing / bad-format / unknown / revoked / expired token; deleted workspace (token caller) |
| 403 | Token valid but role insufficient; deleted workspace (session caller) |
| 404 | Resource doesn't exist OR is in a workspace the caller doesn't own |
| 410 | Invitation already accepted / revoked |
| 422 | Pydantic validation error (request body didn't match the schema) |
| 429 | Rate limited (see limits above) |
| 500 | Server error — please report |
401 responses include a WWW-Authenticate: Bearer error="invalid_token" challenge with the failure reason in error_description. Clients should treat any 401 as "stop retrying with this credential" and either rotate the token or sign in again.
A workspace soft-deleted in the 30-day grace window returns 401 to API-token callers (so a CI loop stops retrying) and 403 to session callers (so the FE can render the "restore" banner). The OWNER restoring the workspace flips both back to 200.
Quick start (curl)
TOKEN=gfat_<your token>
# List sites
curl -H "Authorization: Bearer $TOKEN" \
https://api.greatfeedback.ai/api/v1/sites
# List annotations on a site
curl -H "Authorization: Bearer $TOKEN" \
"https://api.greatfeedback.ai/api/v1/sites/$SITE_ID/annotations?status=new"
# Mark one resolved
curl -X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"resolved","summary":"shipped in v2"}' \
https://api.greatfeedback.ai/api/v1/annotations/$ANN_ID
# Reply on the thread
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"body":"fixed in #1234"}' \
https://api.greatfeedback.ai/api/v1/annotations/$ANN_ID/replies
Token rotation
There's no rotation grace period — revoking a token invalidates it immediately. To rotate:
- Mint a new token in the dashboard.
- Roll the new token into your secret manager.
- Confirm the new token works against
/api/v1/sites. - Revoke the old token.
If you suspect a token leaked, revoke first and ask questions later.
Claude Code skill
A higher-level skill that wraps these endpoints in slash commands ships from a separate repo: great-feedback-skill. The skill speaks MCP for read paths and curls /api/v1/* for writes; an install.sh writes both configs.