Frase Playbooks API
Content playbooks for structured content workflows
Content playbooks for structured content workflows
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-playbooks-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 Playbooks 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: Playbooks
description: Content playbooks for structured content workflows
paths:
/playbooks/{id}/runs:
get:
operationId: listPlaybookRuns
summary: List a playbook's runs
description: Retrieve runs for a playbook, most recent first.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Playbook ID
schema:
type: string
format: uuid
- name: page
in: query
schema:
type: integer
default: 1
- name: page_size
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: List of playbook runs
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
playbook_id:
type: string
format: uuid
status:
type: string
enum:
- pending
- running
- paused
- completed
- failed
- cancelled
topic:
type: string
nullable: true
current_step_index:
type: integer
nullable: true
current_step_name:
type: string
nullable: true
description: The name this run stored for the current step position, resilient to later edits of the playbook definition.
started_at:
type: string
format: date-time
nullable: true
completed_at:
type: string
format: date-time
nullable: true
paused_at:
type: string
format: date-time
nullable: true
error:
type: string
nullable: true
pagination:
$ref: '#/components/schemas/Pagination'
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
description: Rate limited, or the org's monthly brief limit blocked minting the refreshed brief (only the FIRST supersession of a brief is billing-exempt; repeats meter normally)
post:
operationId: startPlaybookRun
summary: Start a playbook run
description: Kick off a run of a playbook. Depending on the playbook's first step you may pass a `topic` (to generate from), an existing `content_id` (to operate on), and/or a `site_id`. A disabled playbook, an exhausted run quota, or too many concurrent runs are surfaced with specific error codes.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Playbook ID
schema:
type: string
format: uuid
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
topic:
type: string
description: Topic to seed the run, when the playbook generates new content.
content_id:
type: string
format: uuid
description: Existing content the run should operate on.
site_id:
type: string
format: uuid
description: Site the run is scoped to, when required by the playbook.
responses:
'201':
description: Run started
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
playbook_id:
type: string
format: uuid
status:
type: string
enum:
- pending
- running
- paused
- completed
- failed
- cancelled
topic:
type: string
nullable: true
current_step_index:
type: integer
nullable: true
current_step_name:
type: string
nullable: true
description: The name this run stored for the current step position, resilient to later edits of the playbook definition.
started_at:
type: string
format: date-time
nullable: true
completed_at:
type: string
format: date-time
nullable: true
description: Always null at start; populated when the run reaches a terminal state.
paused_at:
type: string
format: date-time
nullable: true
description: Always null at start; populated only when the run pauses.
error:
type: string
nullable: true
description: Always null at start; populated only on failure.
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: Your subscription does not include this feature
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/NotFound'
'429':
description: Playbook run limit reached, or too many concurrent runs
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/playbooks/{id}/runs/{runId}:
get:
operationId: getPlaybookRun
summary: Get a playbook run
description: Retrieve a single run with its per-step results and outputs.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Playbook ID
schema:
type: string
format: uuid
- name: runId
in: path
required: true
description: Run ID
schema:
type: string
format: uuid
responses:
'200':
description: Run detail
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id:
type: string
format: uuid
playbook_id:
type: string
format: uuid
content_id:
type: string
format: uuid
nullable: true
status:
type: string
enum:
- pending
- running
- paused
- completed
- failed
- cancelled
topic:
type: string
nullable: true
current_step_index:
type: integer
nullable: true
current_step_name:
type: string
nullable: true
description: The name this run stored for the current step position.
step_results:
type: array
nullable: true
items:
type: object
description: Per-step results captured as the run progressed.
outputs:
nullable: true
description: The run's accumulated outputs.
started_at:
type: string
format: date-time
nullable: true
completed_at:
type: string
format: date-time
nullable: true
paused_at:
type: string
format: date-time
nullable: true
retry_count:
type: integer
nullable: true
error:
type: string
nullable: true
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
description: Rate limited, or the org's monthly brief limit blocked minting the refreshed brief (only the FIRST supersession of a brief is billing-exempt; repeats meter normally)
/playbooks/{id}/runs/{runId}/cancel:
post:
operationId: cancelPlaybookRun
summary: Cancel a playbook run
description: Cancel a run that is still pending, running, or paused. A run that has already reached a terminal state, or finishes between the check and the write, returns 409 `invalid_state`.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Playbook ID
schema:
type: string
format: uuid
- name: runId
in: path
required: true
description: Run ID
schema:
type: string
format: uuid
responses:
'200':
description: Run cancelled
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
cancelled:
type: boolean
run_id:
type: string
format: uuid
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: The run is no longer in a cancellable state
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited, or the org's monthly brief limit blocked minting the refreshed brief (only the FIRST supersession of a brief is billing-exempt; repeats meter normally)
/playbooks/{id}/runs/{runId}/resume:
post:
operationId: resumePlaybookRun
summary: Resume a paused playbook run
description: Provide human-review input to a run paused for review, resuming execution. Only paused runs are resumable; anything else returns 409 `invalid_state`.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
description: Playbook ID
schema:
type: string
format: uuid
- name: runId
in: path
required: true
description: Run ID
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- input
properties:
input:
type: string
description: Required. The reviewer's input the paused step is waiting on.
responses:
'200':
description: Run resuming
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
status:
type: string
enum:
- resuming
run_id:
type: string
format: uuid
request_id:
type: string
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: The run is not in a resumable (paused) state
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited, or the org's monthly brief limit blocked minting the refreshed brief (only the FIRST supersession of a brief is billing-exempt; repeats meter normally)
/playbooks:
get:
operationId: listPlaybooks
summary: List playbooks
description: Retrieve playbooks for your organization.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
minimum: 1
description: Page number
- name: page_size
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
description: Items per page
- name: site_id
in: query
schema:
type: string
format: uuid
description: Filter by site ID
responses:
'200':
description: Paginated list of playbooks
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/PlaybookSummary'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/playbooks/{id}:
get:
operationId: getPlaybook
summary: Get playbook
description: Retrieve a single playbook by ID.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Playbook ID
responses:
'200':
description: Playbook details
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Playbook'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
delete:
operationId: deletePlaybook
summary: Delete playbook
description: Delete a playbook. System playbooks cannot be deleted.
tags:
- Playbooks
security:
- ApiKeyAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Playbook ID
responses:
'200':
description: Playbook deleted
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
request_id:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: System playbook cannot be deleted
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/NotFound'
'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
Playbook:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
nullable: true
steps:
type: array
items:
type: object
step_count:
type: integer
estimated_minutes:
type: integer
nullable: true
is_system:
type: boolean
is_enabled:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
nullable: true
PlaybookSummary:
type: object
description: Playbook as returned in list responses
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
nullable: true
steps:
type: array
items:
type: object
step_count:
type: integer
estimated_minutes:
type: integer
nullable: true
is_system:
type: boolean
is_enabled:
type: boolean
created_at:
type: string
format: date-time
Pagination:
type: object
properties:
page:
type: integer
page_size:
type: integer
total:
type: integer
has_more:
type: boolean
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.'