Frase Comments API
List, resolve, and request AI comments on content articles
List, resolve, and request AI comments on content articles
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/frase-io:frase-io-comments-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Frase Comments API
version: 1.0.0
description: 'The Frase API provides programmatic access to your SEO content platform. Use it to manage content, research, audits, AI visibility tracking, and more. ## Authentication
All API requests require authentication using an API key. Include your key in the `X-API-KEY` header:
```
X-API-KEY: sk_live_your_api_key_here
```
For legacy compatibility, the `token` header is also supported. ## Rate Limits
Three independent limits apply, each with its own headers and error code. Headers are returned on `429` responses, not on successful ones. **Per-minute burst** - 60/min on trial and Starter, 120 Professional, 300 Scale, 1,000 Enterprise. Error code `rate_limit_exceeded`. - `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (epoch seconds)
**Sustained hourly ceiling** - 2,000/hour trial, 3,000 Starter, 5,000 Professional, 15,000 Scale, 50,000 Enterprise. Error code `hourly_limit_exceeded`. - `X-RateLimit-Hourly-Limit`, `X-RateLimit-Hourly-Remaining`, `X-RateLimit-Hourly-Reset` (epoch seconds)
**Monthly request budget** - error code `monthly_budget_exceeded`, headers `X-Monthly-Limit`, `X-Monthly-Used`, `X-Monthly-Remaining`. Every `429` carries `Retry-After` in seconds. On the hourly ceiling this can be up to 3,600, so clients should honour it rather than retrying on a short fixed backoff - a retry sooner than `Retry-After` cannot succeed. ## Errors
The API uses standard HTTP status codes and returns errors in a consistent format:
```json
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "Invalid API key provided.",
"doc_url": "https://docs.frase.io/api/errors#invalid_api_key"
}
}
```
## Changelog
Breaking changes are listed here, newest first. "Breaking" means a field that was always present with a value can now be `null`, or an existing value now means something different — not that a field was removed or renamed. Additive fields are not listed. ### 2026-08-31 — Fact `sources[]` may omit `url`, and minted facts default to `approved`
- `GET /briefs/{id}/facts`, `PATCH /briefs/{id}/facts/{factId}` → a `sources[]` entry may now carry only `title`, with no `url` or `domain`. Research names a publisher far more often than it captures a link (793 of 801 plan-evidence items in production), and those names were previously discarded rather than returned. Read `url` defensively. - `GET /briefs/{id}/facts` → `approval` on a newly minted fact is now `approved` rather than `pending`. A client polling for `pending` facts to review will see far fewer; rejection is unchanged and remains permanent. Facts the writer asserted in its own output (`provenance: generated`) are still minted `pending`. ### 2026-08-24 — Content Guard''s hasPublishedUrl is now recoverability, not the stored URL
- `GET /content/{id}/monitoring` → `hasPublishedUrl`: was a judgment on the STORED URL (false for a relative path even when the page was live). It now answers "could Guard watch this article right now" — the stored URL when absolute, or an address the server recovers (CMS inventory; FraseCMS re-derivation from current settings when a live published post exists). `true` no longer implies the stored URL itself is absolute, and a FraseCMS article published before domain setup flips to `true` when setup finishes, with no re-publish. It remains exactly the condition enabling enforces. - `publishedUrlIssue` is now the RESIDUAL after recovery failed, not a raw-column classification — "not_absolute" can also mean the FraseCMS post is not live or FraseCMS is disabled for the site. - `POST /content/{id}/publish` → `url` (frase_cms): now derived from the site''s current CMS settings rather than the adapter''s host-of-the-moment value; the same URL is stored on the content row and sent in the content.published webhook.'
contact:
name: Frase Support
url: https://frase.io
email: support@frase.io
license:
name: Proprietary
url: https://www.frase.io/company/terms
servers:
- url: https://next.frase.io/api/v1
description: Production
- url: /api/v1
description: This host
security:
- ApiKeyAuth: []
- BearerAuth: []
tags:
- name: Comments
description: List, resolve, and request AI comments on content articles
paths:
/content/{id}/comments:
get:
operationId: listContentComments
summary: List comments on a content article
description: 'Returns a flat list of human + AI comments attached to an article. Clients can rebuild threads via `parent_comment_id` and `root_comment_id`. AI-authored comments visible to the caller are filtered by the same visibility rules used in the app: owners and admins see everything; other members only see AI comments they requested or were @-mentioned in (directly or anywhere in the same thread). API keys without a bound user are treated as admin-equivalent.'
tags:
- Comments
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Content article UUID.
schema:
type: string
format: uuid
- name: status
in: query
description: Filter by comment status. Defaults to `open`.
schema:
type: string
enum:
- open
- resolved
- all
default: open
responses:
'200':
description: Flat array of comments for the article.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
comments:
type: array
items:
$ref: '#/components/schemas/Comment'
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/content/{id}/ai-comments:
post:
operationId: requestAiComments
summary: Request AI suggestion comments on a content article
description: Generates a batch of AI-authored suggestion comments for the article and persists them. Synchronous — returns the created comment IDs and suggestion metadata. Each created comment can subsequently be applied via `POST /content/{id}/ai-comments/{commentId}/apply` or resolved via `PATCH /comments/{commentId}`.
tags:
- Comments
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Content article UUID.
schema:
type: string
format: uuid
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
sidebar_context:
type: string
description: Optional extra context (max ~3000 chars) appended to the reviewer prompt.
responses:
'200':
description: AI suggestion comments created.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
comments:
type: array
items:
$ref: '#/components/schemas/RequestedAiComment'
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/content/{id}/ai-comments/{commentId}/apply:
post:
operationId: applyAiComment
summary: Apply an AI suggestion to the content body
description: Splices the AI comment's `suggested_text` into `content.body` at the comment's anchored selection, then marks the comment as resolved in a single transaction. Refuses to guess when the target text is missing or appears multiple times.
tags:
- Comments
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Content article UUID.
schema:
type: string
format: uuid
- name: commentId
in: path
required: true
description: AI comment UUID.
schema:
type: string
format: uuid
responses:
'200':
description: Suggestion applied. Content body updated and comment resolved.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
applied:
type: boolean
enum:
- true
content_id:
type: string
format: uuid
comment_id:
type: string
format: uuid
word_count:
type: integer
nullable: true
request_id:
type: string
'400':
description: Comment is not an AI suggestion (missing `suggested_text`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: Target text is missing or ambiguous in the draft. Error `code` is one of `target_text_not_found`, `target_text_ambiguous`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
/comments/{id}:
patch:
operationId: setCommentStatus
summary: Resolve or reopen a comment
description: Updates the status of a single comment. AI comments not visible to the caller under the visibility gate return 404.
tags:
- Comments
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Comment UUID.
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- status
properties:
status:
type: string
enum:
- resolved
- open
responses:
'200':
description: Comment status updated.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
comment:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- open
- resolved
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/comments/bulk-resolve:
post:
operationId: bulkSetCommentStatus
summary: Resolve or reopen many comments at once
description: Applies the same status to up to 100 comments in a single request. Comments that are missing, deleted, or hidden by the visibility gate are returned in `skipped`.
tags:
- Comments
security:
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- comment_ids
- status
properties:
comment_ids:
type: array
minItems: 1
maxItems: 100
items:
type: string
format: uuid
status:
type: string
enum:
- resolved
- open
responses:
'200':
description: Bulk update applied.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
updated:
type: array
items:
type: string
format: uuid
skipped:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
reason:
type: string
enum:
- not_found
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
components:
responses:
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
type: invalid_request_error
code: resource_not_found
message: The requested resource was not found.
doc_url: https://docs.frase.io/api/errors#resource_not_found
request_id: req_abc123
Unauthorized:
description: Authentication failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
type: authentication_error
code: invalid_api_key
message: Invalid API key. Check that you are using the correct key.
doc_url: https://docs.frase.io/api/errors#invalid_api_key
request_id: req_abc123
RateLimited:
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
description: Seconds until rate limit resets
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
type: rate_limit_error
code: rate_limit_exceeded
message: Rate limit exceeded. Please retry after 60 seconds.
doc_url: https://docs.frase.io/api/errors#rate_limit_exceeded
request_id: req_abc123
ValidationError:
description: Validation failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
type: invalid_request_error
code: validation_error
message: The request body is invalid.
param: prompt_text
doc_url: https://docs.frase.io/api/errors#validation_error
request_id: req_abc123
schemas:
Error:
type: object
properties:
error:
type: object
properties:
type:
type: string
code:
type: string
message:
type: string
param:
type: string
doc_url:
type: string
request_id:
type: string
RequestedAiComment:
type: object
description: A single AI suggestion comment created by `POST /content/{id}/ai-comments`. Use `id` with the `apply` endpoint or with `PATCH /comments/{id}`.
properties:
id:
type: string
format: uuid
category:
type: string
enum:
- structure
- clarity
- seo
- governance
- engagement
- authority
section:
type: string
quote:
type: string
suggestion:
type: string
suggested_text:
type: string
nullable: true
selection_anchored:
type: boolean
description: True when `quote` was found in the draft's plain-text projection and the comment will be applyable. False means the quote couldn't be located — apply will 409 with `target_text_not_found`.
Comment:
type: object
description: A comment attached to a content article. Human-authored comments have `author.is_system_user = false`; AI-authored comments have `author.is_system_user = true` and populate `ai_suggestion`. Clients rebuild threads via `parent_comment_id` / `root_comment_id`.
properties:
id:
type: string
format: uuid
body:
type: string
description: Markdown body. For AI comments, the AI-metadata prefix is already stripped.
status:
type: string
enum:
- open
- resolved
selection_start:
type: integer
nullable: true
description: Editor position (ProseMirror coordinates) where the anchored selection begins. AI comments created via the public `POST /content/{id}/ai-comments` endpoint return `null` here — use `selection_text` to locate the anchor.
selection_end:
type: integer
nullable: true
description: Editor position where the anchored selection ends. See `selection_start` for nullability notes.
selection_text:
type: string
nullable: true
description: Verbatim text excerpt the comment is anchored to.
content_version:
type: integer
nullable: true
parent_comment_id:
type: string
format: uuid
nullable: true
root_comment_id:
type: string
format: uuid
nullable: true
mentioned_user_ids:
type: array
items:
type: string
format: uuid
author:
type: object
properties:
id:
type: string
format: uuid
nullable: true
name:
type: string
nullable: true
email:
type: string
nullable: true
avatar_url:
type: string
nullable: true
is_system_user:
type: boolean
ai_suggestion:
type: object
nullable: true
description: Present on AI-authored comments. `suggested_text` is the replacement copy that `POST /content/{id}/ai-comments/{commentId}/apply` will splice into the article body.
properties:
category:
type: string
enum:
- structure
- clarity
- seo
- governance
- engagement
- authority
suggested_text:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
nullable: true
resolved_at:
type: string
format: date-time
nullable: true
resolved_by_id:
type: string
format: uuid
nullable: true
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-KEY
description: 'API key for authentication. Get your key from Settings > API Keys. You may also send the same key as `Authorization: Bearer <key>` — see BearerAuth.'
BearerAuth:
type: http
scheme: bearer
bearerFormat: Frase API key (sk_live_* / sk_test_*) or MCP access token (mcp_at_*)
description: 'Send either your API key or an MCP access token as `Authorization: Bearer <token>`. An API key sent this way is exactly equivalent to sending it in X-API-KEY — same identity, same scopes, same rate limits — so use whichever your HTTP client makes natural. X-API-KEY takes precedence if you somehow send both. Cookie/session auth is intentionally NOT supported on /api/v1/* endpoints.'