Frase Audit API
Client telemetry ingest (MCP tool invocations) - distinct from Audits (site audits)
Client telemetry ingest (MCP tool invocations) - distinct from Audits (site audits)
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-audit-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 Audit 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: Audit
description: Client telemetry ingest (MCP tool invocations) - distinct from Audits (site audits)
paths:
/audit/mcp-tool:
post:
operationId: recordMcpToolInvocation
summary: Record an MCP tool invocation
description: 'Append-only telemetry ingest for MCP tool calls. The Frase MCP server (npm/stdio build) posts one row here per tool invocation; the in-app HTTP MCP surface writes the same row directly without the round trip. Rows land in `mcp_tool_invocations`, scoped to the authenticated organization, and are swept by the daily retention job.
No scope is required beyond a valid API key - any key for the organization may write its own telemetry. Only a hash and the size of the tool input are stored, never the input itself, so no customer content reaches this table.
A dedicated per-key rate limit of 100 requests/minute sits on top of the shared tier bucket (keyed on organization plus user, falling back to the key id), so one buggy client cannot drive unbounded row growth between retention sweeps.
Clients should treat this call as fire-and-forget: telemetry must never break a tool call. Every field except `error_code` is required, and unknown fields are ignored.'
tags:
- Audit
security:
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- tool_name
- input_hash
- input_size
- result_status
- duration_ms
properties:
tool_name:
type: string
minLength: 1
maxLength: 100
description: Required. The MCP tool that ran. Trimmed before validation.
input_hash:
type: string
pattern: ^[a-f0-9]{64}$
description: Required. SHA-256 of the JSON-serialized tool input, 64 hex characters. Case-insensitive on the way in; stored lowercased.
input_size:
type: integer
minimum: 0
maximum: 10000000
description: Required. Byte length of the serialized input (UTF-8).
result_status:
type: string
enum:
- success
- error
- rate_limited
- validation_error
- not_found
- unauthorized
- timeout
description: Required. How the tool call ended.
duration_ms:
type: integer
minimum: 0
maximum: 900000
description: Required. Wall-clock duration of the tool call, capped at 15 minutes.
error_code:
type: string
minLength: 1
maxLength: 50
pattern: ^[a-z0-9_\-.]+$
description: Optional. Short machine code for a non-success result. Trimmed; letters, digits, underscore, hyphen and dot only (case-insensitive). Stored as null when omitted.
responses:
'201':
description: Invocation recorded
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
recorded:
type: boolean
description: Always true on 201.
'400':
description: The body was not a JSON object, or a field failed validation. Structured Error with code `invalid_param`; `param` names the first failing field (or `body`) and `message` carries that field's validation message.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
description: 'The per-key ingest limit of 100 requests/minute was exceeded, or the shared API tier/budget limit denied the request. The ingest denial uses the plain `{ "data": { "error": "Rate limit exceeded. Retry after N seconds." } }` envelope and carries no Retry-After header - the retry delay is in the message.'
content:
application/json:
schema:
oneOf:
- type: object
properties:
data:
type: object
properties:
error:
type: string
- $ref: '#/components/schemas/Error'
'500':
description: 'The insert failed (`Failed to record MCP audit event`). Plain `{ "data": { "error": "..." } }` envelope. Clients must not retry in a way that blocks the tool call this row describes.'
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
error:
type: string
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.'