Work with this as data
Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
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.
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/frase-io:frase-io-research-api"
All apis
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.
Get an API 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 Specification
openapi: 3.2.0
info:
title: Frase Research 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: Research
description: AI-powered research
paths:
/research:
get:
operationId: listResearch
summary: List research
description: Retrieve all research sessions in your organization.
tags:
- Research
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: page_size
in: query
schema:
type: integer
default: 20
maximum: 100
- name: site_id
in: query
schema:
type: string
format: uuid
description: Filter research sessions by site ID
- name: briefable
in: query
schema:
type: string
enum:
- 'true'
- 'false'
- '1'
- '0'
description: true restricts the list to sessions POST /briefs will accept right now (finished agent sessions; excludes archived sessions and legacy/migrated research whose ids the brief endpoint cannot resolve or whose status is untrustworthy). Build brief-creation pickers from this filter — an id from the unfiltered list can be a guaranteed 404 or 422. Any other value is a 400.
responses:
'200':
description: List of research sessions
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResearch'
'401':
$ref: '#/components/responses/Unauthorized'
post:
operationId: startResearch
summary: Start research
description: Start AI-powered research on a topic. Research runs asynchronously — use the returned session ID with GET /research/{id} to check progress. The research_url field provides a direct link to view the research in the Frase app.
tags:
- Research
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- query
- site_id
properties:
query:
type: string
minLength: 2
maxLength: 500
description: The topic or search query to research
site_id:
type: string
format: uuid
description: The site to associate the research with (required). Provides brand context, language, and country defaults.
type:
type: string
enum:
- general
- topic
- competitor
- gap
default: general
description: Research type. 'general' runs keywords + SERP + gaps + evidence + AI visibility. 'topic' maps subtopic directions. 'competitor' and 'gap' run the general pipeline with type-specific strategy context.
language:
type: string
description: Language code (e.g., 'en'). Falls back to site language if omitted.
country:
type: string
description: Country code (e.g., 'US'). Falls back to site country if omitted.
competitor_domains:
type: array
items:
type: string
description: Competitor domains to analyze (used with type=competitor or type=gap)
audience:
type: string
description: Target audience context for research
objective:
type: string
description: Content objective — what the research should accomplish
focus:
type: string
description: Focus area or differentiation lens
responses:
'201':
description: Research started
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
query:
type: string
status:
type: string
type:
type: string
nullable: true
language:
type: string
nullable: true
country:
type: string
nullable: true
created_at:
type: string
format: date-time
research_url:
type: string
description: URL to view the research session in the Frase app
request_id:
type: string
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/research/{id}:
get:
operationId: getResearch
summary: Get research
description: Retrieve a specific research session by ID.
tags:
- Research
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Research details
content:
application/json:
schema:
$ref: '#/components/schemas/Research'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteResearch
summary: Delete research
description: Soft delete a research session (moves to trash).
tags:
- Research
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Research deleted
content:
application/json:
schema:
type: object
properties:
deleted:
type: boolean
id:
type: string
format: uuid
'404':
$ref: '#/components/responses/NotFound'
/research/{id}/export:
get:
operationId: exportResearch
summary: Export research
description: Export research findings in JSON or Markdown format.
tags:
- Research
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
- name: format
in: query
schema:
type: string
enum:
- json
- markdown
default: json
responses:
'200':
description: Exported research
content:
application/json:
schema:
$ref: '#/components/schemas/ResearchExport'
text/markdown:
schema:
type: string
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
ResearchSummary:
type: object
properties:
id:
type: string
format: uuid
query:
type: string
status:
type: string
type:
type: string
language:
type: string
nullable: true
country:
type: string
nullable: true
site_id:
type: string
format: uuid
nullable: true
created_at:
type: string
format: date-time
PaginatedResearch:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/ResearchSummary'
pagination:
$ref: '#/components/schemas/Pagination'
request_id:
type: string
Research:
type: object
properties:
id:
type: string
format: uuid
query:
type: string
type:
type: string
enum:
- general
- topic
- competitor
- gap
- serp
nullable: true
status:
type: string
enum:
- pending
- researching
- analyzing
- completed
- failed
nullable: true
depth:
type: string
nullable: true
summary:
type: string
nullable: true
findings:
type: array
items:
$ref: '#/components/schemas/ResearchFinding'
sources:
type: array
items:
$ref: '#/components/schemas/ResearchSource'
site_id:
type: string
format: uuid
nullable: true
project_id:
type: string
format: uuid
nullable: true
language:
type: string
nullable: true
country:
type: string
nullable: true
progress:
type: integer
nullable: true
created_at:
type: string
format: date-time
completed_at:
type: string
format: date-time
nullable: true
data:
type: object
nullable: true
description: Type-specific structured research data. Shape depends on the research type. Null for legacy research sessions.
properties:
keywords:
type: array
description: Keywords with metrics (type=general)
items:
type: object
properties:
keyword:
type: string
search_volume:
type: integer
difficulty:
type: string
enum:
- Low
- Medium
- High
cpc:
type: number
intent:
type: string
nullable: true
category:
type: string
serp_results:
type: array
description: SERP competitor analysis (type=general, gap)
items:
type: object
properties:
url:
type: string
title:
type: string
domain:
type: string
rank:
type: integer
word_count:
type: integer
is_own_site:
type: boolean
is_tracked_competitor:
type: boolean
headings:
type: array
items:
type: object
properties:
type:
type: string
text:
type: string
evidence:
type: array
description: Evidence items (type=general)
items:
type: object
properties:
text:
type: string
type:
type: string
enum:
- stat
- fact
- quote
source:
type: string
nullable: true
sources:
type: array
description: Sources found (type=general)
items:
type: object
properties:
url:
type: string
title:
type: string
domain:
type: string
content_gaps:
type: array
description: Content gaps (type=gap, general)
items:
type: object
properties:
title:
type: string
description:
type: string
impact:
type: string
enum:
- high
- medium
- low
competitors_covering:
type: integer
competitors:
type: array
description: Competitor analysis (type=competitor)
items:
type: object
properties:
domain:
type: string
url:
type: string
title:
type: string
rank:
type: integer
word_count:
type: integer
headings:
type: array
items:
type: object
properties:
type:
type: string
text:
type: string
topic_directions:
type: array
description: Topic exploration directions (type=topic)
items:
type: object
properties:
title:
type: string
description:
type: string
angle:
type: string
estimated_difficulty:
type: string
enum:
- Low
- Medium
- High
sub_topics:
type: array
description: Sub-topic opportunities (type=topic)
items:
type: object
properties:
topic:
type: string
relevance:
type: number
search_volume:
type: integer
keyword_clusters:
type: array
description: Keyword clusters (type=topic)
items:
type: object
properties:
cluster_name:
type: string
keywords:
type: array
items:
type: string
total_volume:
type: integer
ResearchSource:
type: object
properties:
url:
type: string
title:
type: string
snippet:
type: string
domain:
type: string
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
ResearchExport:
type: object
properties:
id:
type: string
format: uuid
query:
type: string
type:
type: string
depth:
type: string
status:
type: string
summary:
type: string
findings:
type: array
items:
$ref: '#/components/schemas/ResearchFinding'
topic_clusters:
type: array
items:
type: object
sources:
type: array
items:
$ref: '#/components/schemas/ResearchSource'
language:
type: string
nullable: true
country:
type: string
nullable: true
created_at:
type: string
format: date-time
completed_at:
type: string
format: date-time
nullable: true
export_format:
type: string
exported_at:
type: string
format: date-time
ResearchFinding:
type: object
properties:
title:
type: string
content:
type: string
relevance:
type: number
source_urls:
type: array
items:
type: string
Pagination:
type: object
properties:
page:
type: integer
page_size:
type: integer
total:
type: integer
has_more:
type: boolean
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
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.'