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.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
"pending" | "blocked" | "resolved" | "won_t_fix" | "duplicate"
pending— needs attention. Every newly-created annotation starts here.blocked— can't move forward right now (waiting on external input, design call, etc.).resolved— done.won_t_fix— closed without action.duplicate— closed because another annotation covers it; pair with asummarylike "duplicate of".
The pre-2026-05-01 intermediate states (new, ack, triaged, in_progress) were folded into pending and removed from the public surface. Writes that still carry one of those values are silently coerced to pending rather than rejected, so older agents keep working. Any other unknown status 400s 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.