Frase Answers API
Frase Answers: ask the site's knowledge base a question (SSE streaming)
Frase Answers: ask the site's knowledge base a question (SSE streaming)
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-answers-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 Answers 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: Answers
description: 'Frase Answers: ask the site''s knowledge base a question (SSE streaming)'
paths:
/answers/ask:
post:
operationId: askAnswers
summary: Ask Frase Answers a question (SSE)
description: 'Developer-API equivalent of the Answers widget''s ask endpoint: the same retrieval and generation pipeline, streamed back as Server-Sent Events. Requires the `answers:ask` scope.
Gates, in the order they run - each returns immediately:
1. API key auth (401 bad/missing key, 403 missing scope, 429 tier budget).
2. The `answers_v1` feature flag, resolved for the organization. Not enabled means 404 `Endpoint not found.` - the route is made to look nonexistent rather than admitting it exists.
3. Body validation: the body must be JSON, `siteId` and `question` are required strings.
4. `siteId` must belong to the authenticated organization and must not be trashed. A cross-org or deleted site returns the same 404 `Site not found.` as one that never existed.
5. Per-site Answers entitlement, resolved against the target site''s tier. An org admin who turned Answers off for the site gets 403 `site_feature_disabled`; a site whose Answers tier does not include developer-API access gets 403 `plan_required`. Trial-plan organizations skip this gate entirely.
6. A widget must already be configured for the site. With none, 400 `site_not_configured` - set one up at /settings/answers first. When several non-revoked widgets exist the oldest is used; they share one knowledge base, and its branding flag drives the credit cap below.
7. The question is sanitized (control characters stripped, whitespace normalized, 1-10,000 characters) and screened for prompt injection. Failures are 400 `invalid_param` or `disallowed_content`.
8. A per-key Answers rate limit of 60 requests/minute, layered on top of the org-wide tier limit. Exceeding it is 429 `rate_limited` with `Retry-After` and `X-RateLimit-*` headers.
Billing: Answers is metered per CONVERSATION, not per message. The first message of a new conversation reserves one Answers conversation credit; follow-ups that pass the returned `conversationId` back in are free ONLY while the conversation is still live: the row must match the same `visitorFingerprint` AND have been active within the last 120 minutes - an idle conversation is silently replaced by a NEW one, which charges another credit. When the cap is reached the just-created conversation is deleted (so it cannot linger as a 0-message row counting against the cap forever) and the call returns 429 with code `usage_limit_reached`, or `subscription_inactive` when the refusal came from the subscription rather than the cap.
Omit `conversationId` to start a new conversation. `visitorFingerprint` is optional here (unlike the widget): it defaults to `api:`, a stable key-scoped identity, so multi-turn continuity works without minting visitor IDs. Once the stream opens the response is always HTTP 200 - failures inside generation arrive as an SSE `error` event followed by a `message-finish` event with `answerType: "error"`. Failures BEFORE the stream opens (credit reservation, conversation setup) surface as a plain 500 with no v1 envelope.'
tags:
- Answers
security:
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- siteId
- question
properties:
siteId:
type: string
format: uuid
description: Required. A site in your organization that has an Answers widget configured. Note this body field is camelCase, not snake_case.
question:
type: string
minLength: 1
maxLength: 10000
description: Required. The visitor question. Sanitized server-side and rejected if it trips prompt-injection detection.
conversationId:
type: string
description: Optional. Continue an existing conversation (returned on the `conversation` SSE event of the first turn). Omit to start a new one - which is the turn that consumes a conversation credit.
visitorFingerprint:
type: string
description: Optional. Identity used to resolve conversations. Defaults to `api:<apiKeyId>`, so each API key keeps its own thread per site.
responses:
'200':
description: 'SSE stream. Events are emitted as `event: <type>` / `data: <json>` pairs. Types, in order: `conversation` (once, first in the normal case - carries `conversationId`, `messageId` and the retrieved `sources`), zero or more `text-delta` (each carrying a `delta` string to append), an optional `error` when generation fails mid-stream, and `message-finish` (once, last). When generation fails before its first token the stream can consist of just `error` + `message-finish`, with no preceding `conversation` frame.'
headers:
Content-Type:
schema:
type: string
example: text/event-stream
Cache-Control:
schema:
type: string
example: no-cache, no-transform
Connection:
schema:
type: string
example: keep-alive
X-Accel-Buffering:
schema:
type: string
example: 'no'
description: Disables proxy buffering so deltas arrive as they are produced.
X-Request-ID:
schema:
type: string
content:
text/event-stream:
schema:
type: string
description: 'Raw SSE frames. The `data` payload of each event type is:
- `conversation`: `{ "conversationId": string, "messageId": string, "sources": [{ "id": integer, "url": string, "title": string, "snippet": string }] }`
- `text-delta`: `{ "delta": string }`
- `error`: `{ "code": string, "message": string }`
- `message-finish`: `{ "answerType": "rag" | "fallback_idk" | "error", "latencyMs": integer, "inputTokens": integer, "outputTokens": integer, "citations": [{ "position": integer, "url": string, "title": string, "snippet": string }], "rephraseSuggestions"?: string[] }`
`rephraseSuggestions` appears only when `answerType` is `fallback_idk` and the rephrase generator returned results. The widget-only `productCta` field is never emitted on this route, which does not pass a CTA configuration into generation. Note SSE payload keys are camelCase - they are the widget''s wire format, reused verbatim.'
example: 'event: conversation
data: {"conversationId":"c1f0...","messageId":"m9a2...","sources":[{"id":1,"url":"https://example.com/pricing","title":"Pricing","snippet":"Plans start at..."}]}
event: text-delta
data: {"delta":"Plans start"}
event: message-finish
data: {"answerType":"rag","latencyMs":1840,"inputTokens":2103,"outputTokens":142,"citations":[{"position":1,"url":"https://example.com/pricing","title":"Pricing","snippet":"Plans start at..."}]}
'
'400':
description: Body was not JSON (`invalid_request`), `siteId` or `question` missing (`invalid_param`), the question failed sanitization (`invalid_param`, e.g. too long - max 10,000 characters), the question tripped prompt-injection detection (`disallowed_content`), or the site has no widget configured (`site_not_configured`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: The key lacks the `answers:ask` scope, Answers is disabled for this site by an organization admin (`site_feature_disabled`), or the site's Answers tier does not include developer-API access (`plan_required`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 'Answers is not enabled for this organization (`not_found`, `Endpoint not found.`) or the site does not exist, is trashed, or belongs to another organization (`not_found`, `Site not found.`, `param: "siteId"`).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Per-key Answers rate limit exceeded - 60 requests per minute (`rate_limited`) - or the organization's Answers conversation cap for the billing period is reached (`usage_limit_reached`), or its subscription is inactive (`subscription_inactive`). On a conversation-cap refusal the conversation that was just created is deleted, so nothing is charged and nothing lingers.
headers:
Retry-After:
schema:
type: integer
description: Seconds until the per-key window resets. Sent on the rate-limit denial only.
X-RateLimit-Limit:
schema:
type: integer
description: Rate-limit branch only; the credit-cap and subscription denials set no headers.
X-RateLimit-Remaining:
schema:
type: integer
description: Rate-limit branch only; the credit-cap and subscription denials set no headers.
X-RateLimit-Reset:
schema:
type: integer
description: Epoch milliseconds at which the window resets. Rate-limit branch only; the credit-cap and subscription denials set no headers.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: A failure BEFORE the SSE stream opened (credit reservation, conversation setup). No v1 envelope - a plain framework error body. Any conversation created by this request is deleted on a pre-stream failure (credit reservation, message write, brand-context resolution), and an overage reservation the credit check committed is released — so nothing is charged. A follow-up's pre-existing conversation is never touched. No API-usage log row is written for this failure.
components:
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
responses:
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
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.'