Authentication
Public API requests must include a valid Bearer token in the Authorization header. Generate API keys from Settings > API Keys in the dashboard. Keys use the bf_mcp_ prefix and can be scoped for read, write, and AI execution access.
Base URL
https://tryroman.app
Request Header
Authorization: Bearer bf_mcp_your_api_key_here
Scopes
| Scope | Use |
|---|---|
| api:read | Read documented resources and analytics. Also covers the read-only AI Visibility API. |
| api:write | Create, update, delete, publish, and start documented non-AI operations. |
| ai:execute | Run endpoints that spend AI credits, such as generation, research, and deep analysis. |
Example: List Articles
curl -X GET https://tryroman.app/api/articles \ -H "Authorization: Bearer bf_mcp_your_api_key_here" \ -H "Content-Type: application/json"
Example: Create an Article
curl -X POST https://tryroman.app/api/articles \
-H "Authorization: Bearer bf_mcp_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"product_id": "your-product-id",
"target_keyword": "best crm for startups",
"word_range": 2500
}'Responses & Errors
Each endpoint below includes a representative success response. JSON endpoints may include additional fields as the resource grows, so clients should ignore unknown fields and key off stable identifiers, status values, and pagination fields.
Success Response Types
| Type | How to handle it |
|---|---|
| 200 OK - JSON | Parse the response body as JSON. List endpoints usually return a data array plus pagination or totals. |
| 202 Accepted - JSON | The job was queued or started. Poll the matching detail/status endpoint, or consume the event stream listed in the endpoint note. |
| text/event-stream | Read Server-Sent Events until a done, completed, failed, or error event. Reconnect only if the job is still non-terminal. |
| application/x-ndjson | Process one JSON object per line. Treat a final done line as success and an error line as a terminal failure. |
| binary file | Use the response blob or stream directly. Do not call JSON parsing on export endpoints. |
Error Response Shape
Errors return JSON. The error field is safe to show in logs or developer tools. The codefield is the best value to branch on when present.
{
"error": "API key requires the ai:execute scope for this endpoint.",
"code": "API_SCOPE_REQUIRED",
"required_scope": "ai:execute",
"required_scopes": ["api:write", "ai:execute"]
}Error Handling Guide
| Status | Retry | What to do |
|---|---|---|
| 400 | No | Fix the request payload or query parameters before sending again. |
| 401 | No | Check that the Bearer token is present, active, and not revoked. |
| 403 | No | Grant the required scope, use a dashboard session, or switch to a documented public endpoint. |
| 404 | No | Verify the resource ID and product/account ownership. |
| 409 | After state changes | Poll the active job or wait until the resource reaches a valid state. |
| 429 | Yes | Back off, honor Retry-After when present, and add jitter before retrying. |
| 500 | Yes | Retry with exponential backoff. Log the method, path, status, and response body. |
| 503 | Yes | Retry with backoff. This can happen when auth, billing, provider, or configuration dependencies are temporarily unavailable. |
Client Pattern
async function romanFetch(path, options = {}) {
const res = await fetch(`https://tryroman.app${path}`, {
...options,
headers: {
Authorization: 'Bearer bf_mcp_your_api_key_here',
'Content-Type': 'application/json',
...(options.headers || {})
}
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
const retryable = [429, 500, 503].includes(res.status)
throw { status: res.status, retryable, ...body }
}
if (res.status === 202) {
return { accepted: true, body: await res.json() }
}
return res.json()
}Webhook Publishing
Custom webhooks let Roman publish an article to any system that can receive an HTTP POST. Configure the destination in Settings > CMS > Custom Webhook, then publish articles from the dashboard.
Publish Flow
| Step | What happens |
|---|---|
| 1. Configure | Add your receiver URL and optional secret in the Custom Webhook settings. |
| 2. Trigger | Publish the article from Roman after the webhook destination has been saved. |
| 3. Send | Roman posts article content and metadata to your URL as JSON, with the body in both content_html and content_markdown. A request body contentHtml value overrides the saved article body for that publish, and the Markdown is derived from it. |
| 4. Confirm | Any 2xx response is treated as success. Roman then marks the article as published and records webhook publish history. |
| 5. Fail | If your endpoint is unreachable or returns a non-2xx response, Roman returns an error to the caller and does not mark the article as published. |
Outbound Request Headers
Content-Type: application/json X-Webhook-Secret: your-secret # only included when configured
Store the same secret in your receiver and compare it with the X-Webhook-Secret header before accepting the payload.
Article Payload
{
"article_id": "uuid",
"article_type": "normal_article",
"article_subtype": "listicle",
"title": "Article headline or keyword",
"headline": "Article headline",
"cover_image_url": "https://cdn.example.com/cover.png",
"meta_title": "SEO meta title",
"meta_description": "SEO meta description",
"target_keyword": "Primary target keyword",
"content_html": "<p>Full article HTML...</p>",
"content_markdown": "## Section heading\n\nFull article as Markdown...",
"status": "complete",
"published_at": "2026-06-29T10:30:00.000Z"
}Payload Fields
| Field | Description |
|---|---|
| article_id | Unique Roman article ID. |
| article_type | ai_visibility_answer for visibility answer articles; otherwise normal_article. |
| article_subtype | Saved Roman article subtype, such as listicle, comparison, or ai_visibility_answer. May be null. |
| title | Headline when available, then meta title, then target keyword. |
| headline | Article headline, or null when unset. |
| cover_image_url | Cover image URL, or null when no cover image is set. |
| meta_title | SEO title, or null when unset. |
| meta_description | SEO description, or null when unset. |
| target_keyword | Primary keyword the article targets. |
| content_html | Full article body exactly as Roman has it saved. Depending on how the article was written this is HTML or Markdown, so use content_markdown when you need one predictable format. |
| content_markdown | The same body as Markdown. Converted when the saved body is HTML, passed through when it is already Markdown. |
| status | Article status at the time the webhook is triggered. |
| published_at | ISO 8601 timestamp generated when Roman sends the webhook. |
Example Receiver
export async function POST(request: Request) {
const secret = request.headers.get('X-Webhook-Secret')
if (secret !== process.env.ROMAN_WEBHOOK_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const article = await request.json()
await publishArticleToCms({
id: article.article_id,
title: article.title,
html: article.content_html,
coverImageUrl: article.cover_image_url,
})
return Response.json({ ok: true })
}Testing and Errors
| Case | Result |
|---|---|
| Test webhook | The settings page sends a small JSON test payload to your URL with the same secret header. |
| No URL | The publish endpoint returns WEBHOOK_NOT_CONFIGURED. |
| No content | Roman rejects the publish until the article has HTML content. |
| Receiver error | Roman returns WEBHOOK_FAILED for non-2xx responses and WEBHOOK_ERROR when the URL cannot be reached. |
Articles
Create, update, generate, publish, and export articles.
/api/articlesList articles
Retrieve a paginated list of articles with optional filters.
| Name | Type | Required | Description |
|---|---|---|---|
| page | number | No | Page number, starting at 0. |
| limit | number | No | Items per page. Default 20. |
| search | string | No | Case-insensitive match against target keyword, search query, headline, meta title, meta description, and published URL. Max 200 characters. |
| statuses | string | No | Comma-separated status filter. |
| product_ids | string | No | Comma-separated product IDs. |
| source | string | No | Comma-separated values for how the article was created, such as content_import. |
| article_type | string | No | seo for standard articles, ai_visibility_answer for AI Visibility answers, or one exact type such as listicle, comparison, how_to_guide, review, or explainer. |
| created_from | string | No | Only articles created on or after this date. YYYY-MM-DD covers the whole day; a full ISO timestamp is used as given. |
| created_to | string | No | Only articles created on or before this date. |
| updated_from | string | No | Only articles last changed on or after this date. |
| updated_to | string | No | Only articles last changed on or before this date. |
| has_published_url | boolean | No | true for articles that record a live URL, false for those that do not. This is not the same as the published status, which many articles reach without anyone recording the URL — use statuses=published for that. |
| has_cover_image | boolean | No | true for articles with a cover image, false for those without. |
| min_word_range | number | No | Lowest target word count to include. |
| max_word_range | number | No | Highest target word count to include. |
| keyword_import_batch_id | string | No | Only articles created from this keyword import batch. |
| content_import_batch_id | string | No | Only articles created from this content import batch. |
| refresh | string | No | Refresh state filter: scheduled, due_soon, in_progress, or failed. |
| sort_by | string | No | created_at (default), updated_at, target_keyword, headline, status, next_refresh_at, or last_refreshed_at. An unknown field falls back to created_at. |
| sort_order | string | No | asc or desc. Default desc. |
| grouped | boolean | No | Set true to return articles grouped by status instead of one flat page. |
| limit_per_group | number | No | Rows per group when grouped is true. Default 10, max 50. |
| include_meta | boolean | No | Set true to include filter metadata such as the available import batches. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| data | Article[] | No | One page of matching articles, without their bodies. |
| data[].id | string | No | Article ID. |
| data[].product_id | string | Yes | Product the article belongs to. |
| data[].target_keyword | string | No | Primary keyword the article targets. |
| data[].search_query | string | Yes | Query used for research. Defaults to the target keyword. |
| data[].word_range | number | No | Target word count. 0 means the length is chosen at generation time. |
| data[].status | string | No | draft, researching, outline_ready, writing, optimizing, complete, or published. |
| data[].source | string | Yes | How the article was created, such as content_import. |
| data[].source_url | string | Yes | URL the article was imported from. |
| data[].article_type | string | Yes | listicle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer. |
| data[].headline | string | Yes | Article headline. |
| data[].meta_title | string | Yes | SEO title. |
| data[].meta_description | string | Yes | SEO description. |
| data[].cover_image_url | string | Yes | Cover image URL. |
| data[].published_url | string | Yes | Live URL, once one has been recorded. |
| data[].unpublished_changes_at | string | Yes | When the article last changed after being published. |
| data[].auto_refresh_enabled | boolean | No | Whether the article is enrolled in scheduled refreshes. |
| data[].refresh_status | string | Yes | idle, queued, refreshing, or failed. |
| data[].last_refreshed_at | string | Yes | When the article was last refreshed. |
| data[].next_refresh_at | string | Yes | When the next refresh is due. |
| data[].keyword_import_batch_id | string | Yes | Keyword import batch the article came from. |
| data[].content_import_batch_id | string | Yes | Content import batch the article came from. |
| data[].created_at | string | No | ISO 8601 creation time. |
| data[].updated_at | string | No | ISO 8601 time of the last change. |
| data[].product | object | Yes | The product, as { id, name }. |
| data[].keyword_import_batch | object | Yes | The keyword batch, as { id, name }. |
| data[].content_import_batch | object | Yes | The content batch, as { id, name }. |
| total | number | No | Total matches across all pages. |
| hasMore | boolean | No | Whether a further page exists. |
| groups | object[] | Yes | Returned instead of data when grouped=true. Each entry is { key, statuses, articles, total }. |
| meta | object | Yes | Returned when include_meta=true, listing the import batches available as filters. |
/api/articlesCreate an article
Create a draft article record. Use the generation endpoints to produce the outline or content.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| target_keyword | string | Yes | Target keyword. |
| search_query | string | No | Search query. Defaults to target_keyword. |
| custom_instructions | string | No | Extra writing instructions. |
| word_range | number | No | Target word count. Default 2000. |
| auto_word_range | boolean | No | Set true to let the length be chosen at generation time. Overrides word_range. |
| article_type | string | No | listicle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer. Chosen automatically when omitted. |
| source_metadata | object | No | Free-form metadata describing where the article idea came from. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Article ID. |
| product_id | string | Yes | Product the article belongs to. |
| target_keyword | string | No | Primary keyword the article targets. |
| search_query | string | Yes | Query used for research. Defaults to the target keyword. |
| word_range | number | No | Target word count. 0 means the length is chosen at generation time. |
| status | string | No | draft, researching, outline_ready, writing, optimizing, complete, or published. |
| source | string | Yes | How the article was created, such as content_import. |
| source_url | string | Yes | URL the article was imported from. |
| article_type | string | Yes | listicle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer. |
| headline | string | Yes | Article headline. |
| meta_title | string | Yes | SEO title. |
| meta_description | string | Yes | SEO description. |
| cover_image_url | string | Yes | Cover image URL. |
| published_url | string | Yes | Live URL, once one has been recorded. |
| unpublished_changes_at | string | Yes | When the article last changed after being published. |
| auto_refresh_enabled | boolean | No | Whether the article is enrolled in scheduled refreshes. |
| refresh_status | string | Yes | idle, queued, refreshing, or failed. |
| last_refreshed_at | string | Yes | When the article was last refreshed. |
| next_refresh_at | string | Yes | When the next refresh is due. |
| keyword_import_batch_id | string | Yes | Keyword import batch the article came from. |
| content_import_batch_id | string | Yes | Content import batch the article came from. |
| created_at | string | No | ISO 8601 creation time. |
| updated_at | string | No | ISO 8601 time of the last change. |
| product | object | Yes | The product, as { id, name }. |
| content_markdown | string | No | The body as Markdown, whichever format it is stored in. Omit with content_markdown=false. |
| blog_content | string | Yes | Body as written by generation. |
| blog_html | string | Yes | Body as last saved in the editor. Takes precedence over blog_content. |
| optimized_content | string | Yes | Body after optimisation, used when blog_html is unset. |
| outline | object | Yes | The outline the article was written from. |
| outline_html | string | Yes | The outline rendered as HTML. |
| custom_instructions | string | Yes | Brief applied while writing. |
| keyword_ranges | string | Yes | Keyword frequency targets used while writing. |
| research_data | array | Yes | Competitor research gathered for the article. |
| expert_feedback | string | Yes | Review feedback recorded against the outline. |
| cta_data | array | Yes | Calls to action placed in the body. |
| link_suggestions | object | Yes | Internal and external link suggestions, including which have been applied. |
| extracted_domains | array | Yes | Domains referenced by the body. |
| linkedin_post | string | Yes | Generated LinkedIn post draft. |
| deep_analysis | object | Yes | Most recent deep analysis of the article. |
| source_metadata | object | Yes | Where the article idea came from. |
| refresh_interval_days | number | No | Days between scheduled refreshes. |
| refresh_error | string | Yes | Why the last refresh failed. |
| refresh_history | array | Yes | Past refreshes, newest first. |
| publish_history | array | Yes | Past publishes, newest first. |
| cms_publish_metadata | object | Yes | Identifiers returned by the CMS on the last publish. |
| cms_field_values | object | Yes | Values for the CMS integration's custom fields. |
| public_share_enabled | boolean | No | Whether a public share link is active. |
| public_share_created_at | string | Yes | When the share link was created. |
| public_share_updated_at | string | Yes | When the share link was last changed. |
| product | object | Yes | The full product, including its writing style. |
200 OK — JSON
{
"id": "article-id",
"target_keyword": "best crm for startups",
"status": "draft"
}/api/articlesBulk update article status
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Article IDs to update. |
| status | string | Yes | New status. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the update was applied. |
| updated | number | No | How many articles changed status. |
200 OK — JSON
{ "success": true, "updated": 5 }/api/articlesBulk delete articles
Permanently delete multiple articles and their associated assets.
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Article IDs to delete. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the articles were deleted. |
| deleted | number | No | How many articles were deleted. |
/api/articles/:idGet an article
Retrieve article content, outline, product, and writing-style details, plus content_markdown: the article body as Markdown however it happens to be stored.
Bodies are saved as HTML or as Markdown depending on how the article was written, so content_markdown is the field to read when you need one predictable format.
| Name | Type | Required | Description |
|---|---|---|---|
| content_markdown | boolean | No | Set false to leave content_markdown out when you only need the stored fields. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Article ID. |
| product_id | string | Yes | Product the article belongs to. |
| target_keyword | string | No | Primary keyword the article targets. |
| search_query | string | Yes | Query used for research. Defaults to the target keyword. |
| word_range | number | No | Target word count. 0 means the length is chosen at generation time. |
| status | string | No | draft, researching, outline_ready, writing, optimizing, complete, or published. |
| source | string | Yes | How the article was created, such as content_import. |
| source_url | string | Yes | URL the article was imported from. |
| article_type | string | Yes | listicle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer. |
| headline | string | Yes | Article headline. |
| meta_title | string | Yes | SEO title. |
| meta_description | string | Yes | SEO description. |
| cover_image_url | string | Yes | Cover image URL. |
| published_url | string | Yes | Live URL, once one has been recorded. |
| unpublished_changes_at | string | Yes | When the article last changed after being published. |
| auto_refresh_enabled | boolean | No | Whether the article is enrolled in scheduled refreshes. |
| refresh_status | string | Yes | idle, queued, refreshing, or failed. |
| last_refreshed_at | string | Yes | When the article was last refreshed. |
| next_refresh_at | string | Yes | When the next refresh is due. |
| keyword_import_batch_id | string | Yes | Keyword import batch the article came from. |
| content_import_batch_id | string | Yes | Content import batch the article came from. |
| created_at | string | No | ISO 8601 creation time. |
| updated_at | string | No | ISO 8601 time of the last change. |
| product | object | Yes | The product, as { id, name }. |
| content_markdown | string | No | The body as Markdown, whichever format it is stored in. Omit with content_markdown=false. |
| blog_content | string | Yes | Body as written by generation. |
| blog_html | string | Yes | Body as last saved in the editor. Takes precedence over blog_content. |
| optimized_content | string | Yes | Body after optimisation, used when blog_html is unset. |
| outline | object | Yes | The outline the article was written from. |
| outline_html | string | Yes | The outline rendered as HTML. |
| custom_instructions | string | Yes | Brief applied while writing. |
| keyword_ranges | string | Yes | Keyword frequency targets used while writing. |
| research_data | array | Yes | Competitor research gathered for the article. |
| expert_feedback | string | Yes | Review feedback recorded against the outline. |
| cta_data | array | Yes | Calls to action placed in the body. |
| link_suggestions | object | Yes | Internal and external link suggestions, including which have been applied. |
| extracted_domains | array | Yes | Domains referenced by the body. |
| linkedin_post | string | Yes | Generated LinkedIn post draft. |
| deep_analysis | object | Yes | Most recent deep analysis of the article. |
| source_metadata | object | Yes | Where the article idea came from. |
| refresh_interval_days | number | No | Days between scheduled refreshes. |
| refresh_error | string | Yes | Why the last refresh failed. |
| refresh_history | array | Yes | Past refreshes, newest first. |
| publish_history | array | Yes | Past publishes, newest first. |
| cms_publish_metadata | object | Yes | Identifiers returned by the CMS on the last publish. |
| cms_field_values | object | Yes | Values for the CMS integration's custom fields. |
| public_share_enabled | boolean | No | Whether a public share link is active. |
| public_share_created_at | string | Yes | When the share link was created. |
| public_share_updated_at | string | Yes | When the share link was last changed. |
| product | object | Yes | The full product, including its writing style. |
200 OK — JSON
{
"id": "article-id",
"target_keyword": "best crm for startups",
"headline": "The 9 Best CRMs for Startups",
"status": "complete",
"content_markdown": "## Why startups outgrow spreadsheets\n\nA spreadsheet stops...",
"outline": { "sections": [] },
"product": { "id": "product-id", "name": "Acme" }
}/api/articles/:idUpdate an article
Send only the fields you want to change.
| Name | Type | Required | Description |
|---|---|---|---|
| target_keyword | string | No | Updated keyword. |
| status | string | No | Updated status. |
| content_html | string | No | Updated HTML content. |
| outline | object | No | Updated outline. |
| headline | string | No | Updated headline. |
| meta_title | string | No | Updated SEO title. |
| meta_description | string | No | Updated SEO description. |
| published_url | string | No | Live URL of the published article. Must be on the product website or a connected Search Console property. |
| source_metadata | object | No | Merged into the existing source metadata rather than replacing it. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Article ID. |
| product_id | string | Yes | Product the article belongs to. |
| target_keyword | string | No | Primary keyword the article targets. |
| search_query | string | Yes | Query used for research. Defaults to the target keyword. |
| word_range | number | No | Target word count. 0 means the length is chosen at generation time. |
| status | string | No | draft, researching, outline_ready, writing, optimizing, complete, or published. |
| source | string | Yes | How the article was created, such as content_import. |
| source_url | string | Yes | URL the article was imported from. |
| article_type | string | Yes | listicle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer. |
| headline | string | Yes | Article headline. |
| meta_title | string | Yes | SEO title. |
| meta_description | string | Yes | SEO description. |
| cover_image_url | string | Yes | Cover image URL. |
| published_url | string | Yes | Live URL, once one has been recorded. |
| unpublished_changes_at | string | Yes | When the article last changed after being published. |
| auto_refresh_enabled | boolean | No | Whether the article is enrolled in scheduled refreshes. |
| refresh_status | string | Yes | idle, queued, refreshing, or failed. |
| last_refreshed_at | string | Yes | When the article was last refreshed. |
| next_refresh_at | string | Yes | When the next refresh is due. |
| keyword_import_batch_id | string | Yes | Keyword import batch the article came from. |
| content_import_batch_id | string | Yes | Content import batch the article came from. |
| created_at | string | No | ISO 8601 creation time. |
| updated_at | string | No | ISO 8601 time of the last change. |
| product | object | Yes | The product, as { id, name }. |
| content_markdown | string | No | The body as Markdown, whichever format it is stored in. Omit with content_markdown=false. |
| blog_content | string | Yes | Body as written by generation. |
| blog_html | string | Yes | Body as last saved in the editor. Takes precedence over blog_content. |
| optimized_content | string | Yes | Body after optimisation, used when blog_html is unset. |
| outline | object | Yes | The outline the article was written from. |
| outline_html | string | Yes | The outline rendered as HTML. |
| custom_instructions | string | Yes | Brief applied while writing. |
| keyword_ranges | string | Yes | Keyword frequency targets used while writing. |
| research_data | array | Yes | Competitor research gathered for the article. |
| expert_feedback | string | Yes | Review feedback recorded against the outline. |
| cta_data | array | Yes | Calls to action placed in the body. |
| link_suggestions | object | Yes | Internal and external link suggestions, including which have been applied. |
| extracted_domains | array | Yes | Domains referenced by the body. |
| linkedin_post | string | Yes | Generated LinkedIn post draft. |
| deep_analysis | object | Yes | Most recent deep analysis of the article. |
| source_metadata | object | Yes | Where the article idea came from. |
| refresh_interval_days | number | No | Days between scheduled refreshes. |
| refresh_error | string | Yes | Why the last refresh failed. |
| refresh_history | array | Yes | Past refreshes, newest first. |
| publish_history | array | Yes | Past publishes, newest first. |
| cms_publish_metadata | object | Yes | Identifiers returned by the CMS on the last publish. |
| cms_field_values | object | Yes | Values for the CMS integration's custom fields. |
| public_share_enabled | boolean | No | Whether a public share link is active. |
| public_share_created_at | string | Yes | When the share link was created. |
| public_share_updated_at | string | Yes | When the share link was last changed. |
| product | object | Yes | The full product, including its writing style. |
/api/articles/:idDelete an article
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the article was deleted. |
200 OK — JSON
{ "success": true }/api/articles/:id/generate-outlineGenerate an outline
Starts outline generation and returns 202 Accepted.
Fire-and-forget. Poll the article status or consume generation-events until a terminal event.
| Name | Type | Required | Description |
|---|---|---|---|
| stream | boolean | No | When true, response includes a progress stream URL. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| accepted | boolean | No | True once the run has been queued. |
| articleId | string | No | The article the run belongs to. |
| stream | object | Yes | Present when stream=true, as { runId, url } pointing at the generation-events stream. |
202 Accepted — JSON
{
"accepted": true,
"articleId": "...",
"stream": { "runId": "...", "url": "/api/articles/.../generation-events?runId=..." }
}/api/articles/:id/generate-contentGenerate article content
Starts content generation from an existing outline and returns 202 Accepted.
Fire-and-forget. Poll the article status or consume generation-events until a terminal event.
| Name | Type | Required | Description |
|---|---|---|---|
| keyword_ranges | object | No | SEO keyword ranges. |
| custom_instructions | string | No | Article brief used while writing. Saved on the article; send an empty string to clear it. |
| stream | boolean | No | When true, response includes a progress stream URL. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| accepted | boolean | No | True once the run has been queued. |
| articleId | string | No | The article the run belongs to. |
| stream | object | Yes | Present when stream=true, as { runId, url } pointing at the generation-events stream. |
/api/articles/:id/generate-articleGenerate full article
Starts outline and content generation in one background run.
Fire-and-forget. Use generation-events for live progress.
| Name | Type | Required | Description |
|---|---|---|---|
| stream | boolean | No | When true, response includes a progress stream URL. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| accepted | boolean | No | True once the run has been queued. |
| articleId | string | No | The article the run belongs to. |
| stream | object | Yes | Present when stream=true, as { runId, url } pointing at the generation-events stream. |
/api/articles/:id/generation-eventsStream article generation progress
Server-Sent Events stream for an accepted outline, content, or full-article generation run.
SSE stream. The stream also emits heartbeat events while the job is running.
| Name | Type | Required | Description |
|---|---|---|---|
| runId | string | No | Generation run ID returned by the start endpoint. |
| kind | string | No | outline or content. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/articles/:id/generate-linksGenerate link suggestions
Return internal and external link suggestions for an article.
| Field | Type | Nullable | Description |
|---|---|---|---|
| link_suggestions | object | No | Internal and external link suggestions, each with its anchor text and target URL. Also saved on the article. |
/api/articles/:id/generate-linkedin-postGenerate LinkedIn post draft
| Name | Type | Required | Description |
|---|---|---|---|
| customVision | string | No | Extra instructions for the post. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| Article | No | The updated article, shaped exactly like GET /api/articles/:id. The draft is in linkedin_post. |
/api/articles/:id/publish-cmsPublish to connected CMS
Publishes an article through the configured CMS integration. Every field is optional; anything you omit falls back to the saved article.
Returns an SSE progress stream.
| Name | Type | Required | Description |
|---|---|---|---|
| contentHtml | string | No | HTML to publish instead of the saved article body. |
| contentMarkdown | string | No | Markdown to publish, for CMS integrations that take Markdown. |
| headline | string | No | Title to publish under. |
| metaTitle | string | No | SEO title. Used as the publish title when headline is absent. |
| metaDescription | string | No | SEO description. Send an empty string to publish without one. |
| imageUrl | string | No | Cover image URL. Send an empty string to publish without one. |
| cmsFieldValues | object | No | Values for the custom fields configured on the CMS integration. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/articles/:id/sync-liveSync from the live page
Replaces the article's content, SEO title and meta description with what is published now, the same as the Sync button on the article page. The article is read from its connected CMS when Roman can read that CMS, otherwise from its published URL (or the URL it was imported from), leaving out sign-up boxes, share links and related posts. The current version is saved to the article's history first. Your CMS and site are only read, never changed.
No body. When the published version already matches, nothing is rewritten and sync.changed is false. Returns 400 when the article has no live URL, 409 while it is being written or refreshed, 422 when the page is missing, moved to a different article, or has no readable article text, and 502 when the site could not be reached. Every error leaves the article unchanged.
| Field | Type | Nullable | Description |
|---|---|---|---|
| article | Article | No | The article after the sync, shaped exactly like GET /api/articles/:id. |
| sync.synced_at | string | No | When the article last changed to match the published version. |
| sync.checked_at | string | No | When the published version was read this time. |
| sync.url | string | No | The address the article was read from. |
| sync.via | string | No | cms when read from the connected CMS; direct when the page was read as served; fallback when it had to be read in a browser, for pages built with JavaScript or that turn away plain requests. |
| sync.trigger | string | No | manual for this endpoint; auto for syncs the product's Live Sync schedule ran. |
| sync.word_count | number | No | Words in the synced article. |
| sync.changed | boolean | No | False when the published version already matched, so nothing was written. |
| sync.flags | string[] | No | Warnings that part of the page may be missing. Check the article before publishing it when this is not empty. |
| sync.removed_blocks | number | No | Parts of the page that were not the article and were left out. |
| sync.revision | number | Yes | The history entry holding the version before this sync, or null when nothing changed. |
/api/articles/:id/publish-webhookPublish to your webhook
Posts the article to the custom webhook URL saved in Settings. The payload carries the body both as saved (content_html) and as Markdown (content_markdown). See the Webhook Publishing section for the full payload and headers.
Returns 400 with code WEBHOOK_NOT_CONFIGURED when no URL is saved, and 502 with WEBHOOK_FAILED or WEBHOOK_ERROR when your receiver rejects the request or cannot be reached.
| Name | Type | Required | Description |
|---|---|---|---|
| contentHtml | string | No | HTML to send instead of the saved article body. content_markdown is derived from it. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when your receiver accepted the article. |
| message | string | No | Human-readable confirmation. |
| webhook_status | number | No | The HTTP status your receiver returned. |
200 OK — JSON
{
"success": true,
"message": "Article published via webhook successfully.",
"webhook_status": 200
}/api/articles/:id/export-docxExport as DOCX
Returns a binary DOCX file.
| Name | Type | Required | Description |
|---|---|---|---|
| html | string | Yes | HTML content to export. |
| title | string | No | Document title. |
200 OK — file download
A binary response body. Read it as a blob or a stream; do not parse it as JSON.
/api/articles/:id/assetsList article assets
| Field | Type | Nullable | Description |
|---|---|---|---|
| assets | object[] | No | Assets attached to the article. |
| assets[].id | string | No | Asset ID. |
| assets[].type | string | No | Asset kind, such as image or pdf. |
| assets[].status | string | No | pending, approved, or rejected. |
/api/articles/:id/assetsUpdate asset status
| Name | Type | Required | Description |
|---|---|---|---|
| assetId | string | Yes | Asset ID. |
| status | string | Yes | pending, approved, or rejected. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the asset was updated. |
/api/articles/:id/assetsDelete article assets
| Name | Type | Required | Description |
|---|---|---|---|
| assetId | string | No | Specific asset ID to delete. |
| type | string | No | Delete all assets of this type. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the assets were deleted. |
/api/articles/bulk-writeBulk generate articles
Generate content for multiple articles.
Returns one SSE session_created acknowledgement with HTTP 202, then closes. Progress is stored against the session, so poll GET /api/articles/bulk-write/:sessionId. Only one bulk run can be active per account at a time; starting a second returns 409 with the active session.
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Article IDs to process. |
| mode | string | No | outline_ready_content (default) writes Outline Ready articles; draft_full_article researches, outlines, and writes Draft articles. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/articles/bulk-writeList bulk write sessions
Returns the 20 most recent sessions with their items.
| Field | Type | Nullable | Description |
|---|---|---|---|
| [] | BulkWriteSession[] | No | The 20 most recent sessions, newest first, each shaped like the session detail response below. |
/api/articles/bulk-write/:sessionIdGet bulk write session
Returns one session with its items. Session status is queued or preparing while the run is waiting to start, running once it is under way, then completed, stopped, or failed.
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Session ID. |
| status | string | No | queued or preparing while waiting to start, then running, completed, stopped, or failed. |
| mode | string | Yes | outline_ready_content or draft_full_article. |
| product_id | string | Yes | Product the run belongs to. |
| total_articles | number | No | Articles in the run. |
| completed_articles | number | No | Articles finished so far. |
| failed_articles | number | No | Articles that failed. |
| error_message | string | Yes | Why the run failed. |
| started_at | string | Yes | When the run began. |
| completed_at | string | Yes | When the run finished. |
| items | object[] | No | One entry per article in the run. |
| items[].id | string | No | Item ID. |
| items[].article_id | string | No | The article being written. |
| items[].target_keyword | string | No | That article’s keyword. |
| items[].status | string | No | pending, researching, generating_outline, writing, completed, failed, or cancelled. |
| items[].step | string | Yes | The step currently running. |
| items[].error_message | string | Yes | Why this article failed. |
200 OK — JSON
{
"id": "session-id",
"status": "running",
"mode": "outline_ready_content",
"total_articles": 5,
"completed_articles": 2,
"failed_articles": 0,
"items": [
{ "id": "item-id", "article_id": "...", "target_keyword": "best crm", "status": "completed", "step": null }
]
}/api/articles/bulk-write/:sessionIdStop a bulk write session
Stops an active run and cancels the items that have not started. Articles already written are kept.
Returns 400 when the run has already finished.
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the run was stopped. |
200 OK — JSON
{ "success": true }/api/articles/bulk-publish-cmsBulk publish to CMS
Returns an SSE stream with progress.
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Article IDs to publish. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}Products
Manage products and product research inputs.
/api/productsList products
Returns an array of the products this key can reach, newest first.
| Field | Type | Nullable | Description |
|---|---|---|---|
| [] | Product[] | No | Every product this key can reach, newest first. |
| [].id | string | No | Product ID. |
| [].name | string | No | Product name. |
| [].description | string | Yes | What the product does. |
| [].status | string | Yes | Whether the product is set up and active. |
| [].core_problem | string | Yes | The problem the product solves. |
| [].positioning_statement | string | Yes | One-line positioning. |
| [].differentiators | array | Yes | What sets the product apart, as { title, description }. |
| [].competitor_differentiators | array | Yes | How the product differs from named competitors. |
| [].customer_stories | array | Yes | Case studies and customer quotes, as { kind: "case_study" | "quote", customer, quote?, person?, role?, summary?, results?, source_url? }. Kept in step with the site's customer pages weekly. |
| [].customer_stories_checked_at | string | Yes | When the site was last checked for new or changed customer stories. |
| [].features | array | Yes | Product features. |
| [].use_cases | string | Yes | What people use the product for. |
| [].target_audience | array | Yes | Who the product is for. |
| [].value_propositions | array | Yes | The value the product promises. |
| [].content_pillars | array | Yes | Themes the content programme is built around. |
| [].security_compliance | array | Yes | Certifications and compliance claims. |
| [].pricing_info | string | Yes | Pricing summary. |
| [].pricing_details | string | Yes | Full pricing detail. |
| [].pricing_url | string | Yes | Pricing page URL. |
| [].sitemap_url | string | Yes | Website or sitemap URL. |
| [].site_structure | object | Yes | The site sections found on the website. |
| [].cta_signup_url | string | Yes | Where calls to action send readers. |
| [].cta_button_text | string | Yes | Call-to-action button label. |
| [].cta_image_url | string | Yes | Call-to-action image. |
| [].edge_doc | object | Yes | The positioning document the writer works from. |
| [].writing_styles | object | Yes | The saved writing style. |
| [].linkedin_writing_rules | string | Yes | Extra rules for LinkedIn posts. |
| [].publishing_cadence_per_week | number | Yes | Articles planned per week. |
| [].publishing_start_day_of_week | number | Yes | Day of week the schedule starts, 0 for Sunday. |
| [].refresh_publish_mode | string | Yes | How refreshed articles are republished. |
| [].topic_auto_topup | boolean | Yes | Whether the topic backlog tops itself up. |
| [].topic_auto_topup_min_backlog | number | Yes | Backlog size that triggers a top-up. |
| [].search_location_code | number | Yes | Default location code for keyword research. |
| [].search_language_code | string | Yes | Default language code for keyword research. |
| [].cover_style_prompt | string | Yes | Prompt describing the cover image style. |
| [].cover_style_profile | object | Yes | The saved cover image style. |
| [].cover_style_profile_updated_at | string | Yes | When the cover style last changed. |
| [].cover_style_selection_mode | string | Yes | How the cover style is chosen. |
| [].cover_style_suggestions | array | Yes | Suggested cover styles. |
| [].cover_style_updated_at | string | Yes | When the cover style prompt last changed. |
| [].created_at | string | No | ISO 8601 creation time. |
| [].updated_at | string | No | ISO 8601 time of the last change. |
/api/productsCreate a product
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Product name. |
| description | string | No | Product description. |
| sitemap_url | string | No | Website or sitemap URL. |
| writing_style | object | No | Initial writing style. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Product ID. |
| name | string | No | Product name. |
| description | string | Yes | What the product does. |
| status | string | Yes | Whether the product is set up and active. |
| core_problem | string | Yes | The problem the product solves. |
| positioning_statement | string | Yes | One-line positioning. |
| differentiators | array | Yes | What sets the product apart, as { title, description }. |
| competitor_differentiators | array | Yes | How the product differs from named competitors. |
| customer_stories | array | Yes | Case studies and customer quotes, as { kind: "case_study" | "quote", customer, quote?, person?, role?, summary?, results?, source_url? }. Kept in step with the site's customer pages weekly. |
| customer_stories_checked_at | string | Yes | When the site was last checked for new or changed customer stories. |
| features | array | Yes | Product features. |
| use_cases | string | Yes | What people use the product for. |
| target_audience | array | Yes | Who the product is for. |
| value_propositions | array | Yes | The value the product promises. |
| content_pillars | array | Yes | Themes the content programme is built around. |
| security_compliance | array | Yes | Certifications and compliance claims. |
| pricing_info | string | Yes | Pricing summary. |
| pricing_details | string | Yes | Full pricing detail. |
| pricing_url | string | Yes | Pricing page URL. |
| sitemap_url | string | Yes | Website or sitemap URL. |
| site_structure | object | Yes | The site sections found on the website. |
| cta_signup_url | string | Yes | Where calls to action send readers. |
| cta_button_text | string | Yes | Call-to-action button label. |
| cta_image_url | string | Yes | Call-to-action image. |
| edge_doc | object | Yes | The positioning document the writer works from. |
| writing_styles | object | Yes | The saved writing style. |
| linkedin_writing_rules | string | Yes | Extra rules for LinkedIn posts. |
| publishing_cadence_per_week | number | Yes | Articles planned per week. |
| publishing_start_day_of_week | number | Yes | Day of week the schedule starts, 0 for Sunday. |
| refresh_publish_mode | string | Yes | How refreshed articles are republished. |
| topic_auto_topup | boolean | Yes | Whether the topic backlog tops itself up. |
| topic_auto_topup_min_backlog | number | Yes | Backlog size that triggers a top-up. |
| search_location_code | number | Yes | Default location code for keyword research. |
| search_language_code | string | Yes | Default language code for keyword research. |
| cover_style_prompt | string | Yes | Prompt describing the cover image style. |
| cover_style_profile | object | Yes | The saved cover image style. |
| cover_style_profile_updated_at | string | Yes | When the cover style last changed. |
| cover_style_selection_mode | string | Yes | How the cover style is chosen. |
| cover_style_suggestions | array | Yes | Suggested cover styles. |
| cover_style_updated_at | string | Yes | When the cover style prompt last changed. |
| created_at | string | No | ISO 8601 creation time. |
| updated_at | string | No | ISO 8601 time of the last change. |
/api/products/:idGet a product
Returns the product with its writing style.
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Product ID. |
| name | string | No | Product name. |
| description | string | Yes | What the product does. |
| status | string | Yes | Whether the product is set up and active. |
| core_problem | string | Yes | The problem the product solves. |
| positioning_statement | string | Yes | One-line positioning. |
| differentiators | array | Yes | What sets the product apart, as { title, description }. |
| competitor_differentiators | array | Yes | How the product differs from named competitors. |
| customer_stories | array | Yes | Case studies and customer quotes, as { kind: "case_study" | "quote", customer, quote?, person?, role?, summary?, results?, source_url? }. Kept in step with the site's customer pages weekly. |
| customer_stories_checked_at | string | Yes | When the site was last checked for new or changed customer stories. |
| features | array | Yes | Product features. |
| use_cases | string | Yes | What people use the product for. |
| target_audience | array | Yes | Who the product is for. |
| value_propositions | array | Yes | The value the product promises. |
| content_pillars | array | Yes | Themes the content programme is built around. |
| security_compliance | array | Yes | Certifications and compliance claims. |
| pricing_info | string | Yes | Pricing summary. |
| pricing_details | string | Yes | Full pricing detail. |
| pricing_url | string | Yes | Pricing page URL. |
| sitemap_url | string | Yes | Website or sitemap URL. |
| site_structure | object | Yes | The site sections found on the website. |
| cta_signup_url | string | Yes | Where calls to action send readers. |
| cta_button_text | string | Yes | Call-to-action button label. |
| cta_image_url | string | Yes | Call-to-action image. |
| edge_doc | object | Yes | The positioning document the writer works from. |
| writing_styles | object | Yes | The saved writing style. |
| linkedin_writing_rules | string | Yes | Extra rules for LinkedIn posts. |
| publishing_cadence_per_week | number | Yes | Articles planned per week. |
| publishing_start_day_of_week | number | Yes | Day of week the schedule starts, 0 for Sunday. |
| refresh_publish_mode | string | Yes | How refreshed articles are republished. |
| topic_auto_topup | boolean | Yes | Whether the topic backlog tops itself up. |
| topic_auto_topup_min_backlog | number | Yes | Backlog size that triggers a top-up. |
| search_location_code | number | Yes | Default location code for keyword research. |
| search_language_code | string | Yes | Default language code for keyword research. |
| cover_style_prompt | string | Yes | Prompt describing the cover image style. |
| cover_style_profile | object | Yes | The saved cover image style. |
| cover_style_profile_updated_at | string | Yes | When the cover style last changed. |
| cover_style_selection_mode | string | Yes | How the cover style is chosen. |
| cover_style_suggestions | array | Yes | Suggested cover styles. |
| cover_style_updated_at | string | Yes | When the cover style prompt last changed. |
| created_at | string | No | ISO 8601 creation time. |
| updated_at | string | No | ISO 8601 time of the last change. |
/api/products/:idUpdate a product
Send only the product fields you want to change.
Writing style edits are revision-checked: 428 with code REVISION_REQUIRED when expected_revision is missing, 409 with code WRITING_STYLE_CONFLICT when someone saved first. Both responses carry the current revision.
| Name | Type | Required | Description |
|---|---|---|---|
| writing_style | object | No | Replaces the saved writing style. |
| expected_revision | number | No | Revision the writing style edit is based on. Accepted at the top level or inside writing_style. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Product ID. |
| name | string | No | Product name. |
| description | string | Yes | What the product does. |
| status | string | Yes | Whether the product is set up and active. |
| core_problem | string | Yes | The problem the product solves. |
| positioning_statement | string | Yes | One-line positioning. |
| differentiators | array | Yes | What sets the product apart, as { title, description }. |
| competitor_differentiators | array | Yes | How the product differs from named competitors. |
| customer_stories | array | Yes | Case studies and customer quotes, as { kind: "case_study" | "quote", customer, quote?, person?, role?, summary?, results?, source_url? }. Kept in step with the site's customer pages weekly. |
| customer_stories_checked_at | string | Yes | When the site was last checked for new or changed customer stories. |
| features | array | Yes | Product features. |
| use_cases | string | Yes | What people use the product for. |
| target_audience | array | Yes | Who the product is for. |
| value_propositions | array | Yes | The value the product promises. |
| content_pillars | array | Yes | Themes the content programme is built around. |
| security_compliance | array | Yes | Certifications and compliance claims. |
| pricing_info | string | Yes | Pricing summary. |
| pricing_details | string | Yes | Full pricing detail. |
| pricing_url | string | Yes | Pricing page URL. |
| sitemap_url | string | Yes | Website or sitemap URL. |
| site_structure | object | Yes | The site sections found on the website. |
| cta_signup_url | string | Yes | Where calls to action send readers. |
| cta_button_text | string | Yes | Call-to-action button label. |
| cta_image_url | string | Yes | Call-to-action image. |
| edge_doc | object | Yes | The positioning document the writer works from. |
| writing_styles | object | Yes | The saved writing style. |
| linkedin_writing_rules | string | Yes | Extra rules for LinkedIn posts. |
| publishing_cadence_per_week | number | Yes | Articles planned per week. |
| publishing_start_day_of_week | number | Yes | Day of week the schedule starts, 0 for Sunday. |
| refresh_publish_mode | string | Yes | How refreshed articles are republished. |
| topic_auto_topup | boolean | Yes | Whether the topic backlog tops itself up. |
| topic_auto_topup_min_backlog | number | Yes | Backlog size that triggers a top-up. |
| search_location_code | number | Yes | Default location code for keyword research. |
| search_language_code | string | Yes | Default language code for keyword research. |
| cover_style_prompt | string | Yes | Prompt describing the cover image style. |
| cover_style_profile | object | Yes | The saved cover image style. |
| cover_style_profile_updated_at | string | Yes | When the cover style last changed. |
| cover_style_selection_mode | string | Yes | How the cover style is chosen. |
| cover_style_suggestions | array | Yes | Suggested cover styles. |
| cover_style_updated_at | string | Yes | When the cover style prompt last changed. |
| created_at | string | No | ISO 8601 creation time. |
| updated_at | string | No | ISO 8601 time of the last change. |
/api/products/:idDelete a product
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the product was deleted. |
200 OK — JSON
{ "success": true }/api/products/analyze-urlAnalyze product URL
Extract product details from a website.
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Website URL. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| name | string | Yes | Product name read off the site. |
| description | string | Yes | What the product does. |
| core_problem | string | Yes | The problem it solves. |
| positioning_statement | string | Yes | One-line positioning. |
| differentiators | array | No | What sets it apart. |
| competitor_differentiators | array | No | How it differs from named competitors. |
| features | array | No | Features found on the site. |
| target_audience | array | No | Who it is for. |
| value_propositions | array | No | The value it promises. |
| use_cases | string | Yes | What people use it for. |
| security_compliance | array | No | Certifications and compliance claims. |
| pricing_info | string | Yes | Pricing summary. |
| pricing_details | string | Yes | Full pricing detail. |
| pricing_url | string | Yes | Pricing page URL. |
| cta_signup_url | string | Yes | Sign-up URL found on the site. |
| cta_button_text | string | Yes | Call-to-action label. |
| sitemap_url | string | Yes | Sitemap discovered for the site. |
/api/products/analyze-writing-styleExtract writing style
| Name | Type | Required | Description |
|---|---|---|---|
| urls | string[] | Yes | Blog URLs to analyze. Max 5. |
| productName | string | No | Product name for context. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| content_style_rules | string | No | The writing style read off the sample posts, merged with the baseline. |
| analyzed_count | number | No | How many posts were read. |
| failed_urls | string[] | No | URLs that could not be fetched. |
/api/products/suggest-positioningSuggest positioning
| Name | Type | Required | Description |
|---|---|---|---|
| product | object | Yes | Product details. |
| answers | object | No | Optional questionnaire answers. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| positioning_statement | string | Yes | Suggested one-line positioning. |
| differentiators | array | No | Suggested differentiators. |
| competitor_differentiators | array | No | Suggested competitor contrasts. |
/api/products/extract-documentExtract product data from document
Send as multipart/form-data.
| Name | Type | Required | Description |
|---|---|---|---|
| file | File | Yes | PDF, TXT, or Markdown file. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| features | array | No | Features found in the document. |
| differentiators | array | No | Differentiators found in the document. |
| pricing_details | string | Yes | Pricing detail found in the document. |
| positioning_statement | string | Yes | Positioning the document implies. |
| competitor_differentiators | array | No | How the product differs from each competitor the document names. |
| customer_stories | array | No | Case studies and customer quotes in the document. A quote is kept only when it appears in the document word for word. |
Keywords
Research keywords, inspect SERPs, and move keywords into import batches.
/api/keywords/researchRun keyword research
| Name | Type | Required | Description |
|---|---|---|---|
| keywords | string[] | No | One to five seed keywords. Use this or seed_keyword. |
| seed_keyword | string | No | Legacy single seed keyword. Use this or keywords. |
| product_id | string | No | Product ID used to associate saved research and dedupe. |
| language_code | string | No | Language code. Defaults to en. |
| location_code | number | No | Numeric location code. Defaults to United States (2840). |
| Field | Type | Nullable | Description |
|---|---|---|---|
| query_id | string | Yes | The saved query, or null when nothing usable was found. Read it back with GET /api/keywords/research/:queryId. |
| seed_keyword | string | No | The seed the research ran from. |
| total_results | number | No | How many keywords were kept. |
| no_results | boolean | No | True when the research found nothing usable. |
| ai_added | number | No | Keywords added on top of the provider results. |
| validated_ai_added | number | No | How many of those survived validation. |
| results | object[] | No | The keywords found, highest search volume first. |
| results[].id | string | No | Result ID, used to add the keyword to an import batch. |
| results[].keyword | string | No | The keyword. |
| results[].search_volume | number | Yes | Monthly searches. |
| results[].keyword_difficulty | number | Yes | How hard the keyword is to rank for. |
| results[].cpc | number | Yes | Cost per click. |
| results[].competition | number | Yes | Paid competition, 0 to 1. |
| results[].search_intent | string | Yes | informational, navigational, commercial, or transactional. |
| results[].serp_data | object | Yes | Cached results page detail, when one has been fetched. |
/api/keywords/research/historyList keyword research history
Returns the 50 most recent research queries, newest first.
| Field | Type | Nullable | Description |
|---|---|---|---|
| queries | object[] | No | The 50 most recent research queries, newest first. |
| queries[].id | string | No | Query ID. |
| queries[].seed_keyword | string | No | The seed the research started from. |
| queries[].language_code | string | Yes | Language the research ran in. |
| queries[].location_code | number | Yes | Location the research ran in. |
| queries[].total_results | number | Yes | How many keywords the query found. |
| queries[].created_at | string | No | When the research ran. |
| queries[].product | object | Yes | The product, as { id, name }. |
200 OK — JSON
{
"queries": [
{ "id": "query-id", "seed_keyword": "best crm", "total_results": 412, "created_at": "2026-09-01T10:00:00.000Z" }
]
}/api/keywords/research/:queryIdGet keyword research results
Returns the saved query and its keyword rows, highest search volume first.
| Field | Type | Nullable | Description |
|---|---|---|---|
| query | object | No | The saved query, as listed in the research history. |
| results | object[] | No | Its keyword rows, highest search volume first. |
200 OK — JSON
{
"query": { "id": "query-id", "seed_keyword": "best crm" },
"results": [{ "id": "result-id", "keyword": "best crm for startups", "search_volume": 1900 }]
}/api/keywords/research/add-to-importAdd keyword results to import
| Name | Type | Required | Description |
|---|---|---|---|
| result_ids | string[] | Yes | Keyword result IDs. |
| product_id | string | Yes | Product ID. |
| batch_id | string | No | Existing batch to add to. A new batch is created when omitted. |
| batch_name | string | No | Name for the new batch. |
| research_query_id | string | No | Research query the results came from. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| batch_id | string | No | The batch the keywords were added to, new or existing. |
| batch_name | string | Yes | That batch's name. |
| keywords_added | number | No | How many keywords were added. |
/api/keywords/research/serpFetch the live Google results page (results in order; competitors = organic domains)
| Name | Type | Required | Description |
|---|---|---|---|
| keyword | string | Yes | Keyword to check. |
| result_id | string | No | Research result to cache the SERP snapshot on. Cached snapshots are returned free for 30 days. |
| language_code | string | No | Defaults to the research query language, else en. |
| location_code | number | No | Defaults to the research query location, else 2840 (US). |
| Field | Type | Nullable | Description |
|---|---|---|---|
| results | object[] | No | The organic results in the order Google returned them. |
| competitors | string[] | No | The organic domains, in rank order. |
| serp_features | object | Yes | Which result features the page carries. |
| cached | boolean | Yes | True when a snapshot from the last 30 days was reused at no cost. |
Bulk Import
Create keyword batches and generate articles in bulk.
/api/bulk-importList keyword import batches
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID filter. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| [] | KeywordImportBatch[] | No | Batches for the product, newest first. |
| [].id | string | No | Batch ID. |
| [].name | string | Yes | Batch name. |
| [].product_id | string | No | Product the batch belongs to. |
| [].plan_type | string | No | seo_keywords or ai_visibility_answers. |
| [].research_query_id | string | Yes | Research query the batch came from. |
| [].created_at | string | No | When the batch was created. |
| [].updated_at | string | No | When the batch last changed. |
| [].keyword_imports | object[] | No | The keyword rows in the batch. |
/api/bulk-importCreate keyword import batch
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| keywords | object[] | Yes | Keyword rows. |
| name | string | No | Batch name. |
| plan_type | string | No | seo_keywords (default) or ai_visibility_answers. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| KeywordImportBatch | No | The new batch, returned with HTTP 201, shaped as in the batch detail response. | |
| keyword_imports | object[] | No | The keyword rows that were created with it. |
/api/bulk-import/batch/:batchIdGet import batch
Returns the batch with its keyword_imports rows.
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Batch ID. |
| name | string | Yes | Batch name. |
| product_id | string | No | Product the batch belongs to. |
| plan_type | string | No | seo_keywords or ai_visibility_answers. |
| keyword_imports | object[] | No | The keyword rows in the batch. |
| keyword_imports[].id | string | No | Row ID, used to update or generate from it. |
| keyword_imports[].keyword | string | No | The keyword. |
| keyword_imports[].status | string | No | Row status. |
| keyword_imports[].angle | string | Yes | Angle the article should take. |
| keyword_imports[].word_range | number | Yes | Target word count for the article. |
| keyword_imports[].custom_version | string | Yes | Custom instructions for the article. |
/api/bulk-import/batch/:batchIdRename an import batch
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | New batch name. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Batch ID. |
| name | string | No | The new batch name. |
/api/bulk-import/batch/:batchIdDelete an import batch
Deletes the batch and the keyword rows in it.
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the batch and its rows were deleted. |
200 OK — JSON
{ "success": true }/api/bulk-import/:idUpdate a keyword import row
Returns 400 when the body contains none of these fields.
| Name | Type | Required | Description |
|---|---|---|---|
| custom_version | string | No | Custom instructions for this keyword. |
| angle | string | No | Article angle for this keyword. |
| word_range | number | No | Target word count for this keyword. |
| status | string | No | Row status. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Row ID. |
| keyword | string | No | The keyword. |
| status | string | No | Row status after the update. |
| angle | string | Yes | Angle after the update. |
| word_range | number | Yes | Target word count after the update. |
| custom_version | string | Yes | Custom instructions after the update. |
/api/bulk-import/:idDelete a keyword import row
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the row was deleted. |
200 OK — JSON
{ "success": true }/api/bulk-import/generateGenerate from batch
Returns an SSE stream with progress.
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Keyword import IDs to process. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}Content Import
Import existing content from sitemaps or connected CMS providers.
/api/content-importList content import batches
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID filter. |
| page | number | No | Page number, starting at 1. |
| page_size | number | No | Items per page. Default 50, max 100. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| batches | object[] | No | One page of import batches, newest first. |
| batches[].id | string | No | Batch ID. |
| batches[].name | string | Yes | Batch name. |
| batches[].product_id | string | No | Product the batch belongs to. |
| batches[].source_type | string | No | sitemap or the CMS provider it came from. |
| batches[].source_url | string | Yes | The sitemap or site the batch read. |
| batches[].created_at | string | No | When the batch was created. |
| batches[].content_imports | object[] | No | The pages found in the batch. |
| total | number | No | Total batches across all pages. |
| page | number | No | The page returned. |
| page_size | number | No | Rows per page. |
| has_more | boolean | No | Whether a further page exists. |
200 OK — JSON
{
"batches": [{ "id": "batch-id", "source_type": "sitemap", "status": "completed" }],
"total": 12,
"page": 1,
"page_size": 50,
"has_more": false
}/api/content-import/batchCreate content import batch
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| source_type | string | Yes | sitemap or CMS provider. |
| sitemap_url | string | No | Sitemap URL for sitemap imports. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| ContentImportBatch | No | The new batch, returned with HTTP 201, shaped as in the batch detail response. |
/api/content-import/batch/:batchIdGet content import batch
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Batch ID. |
| name | string | Yes | Batch name. |
| source_type | string | No | sitemap or the CMS provider it came from. |
| status | string | Yes | Where the batch has got to. |
| content_imports | object[] | No | The pages in the batch, each with its URL, title and import state. |
/api/content-import/batch/:batchIdDelete content import batch
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the batch was deleted. |
200 OK — JSON
{ "success": true }/api/content-import/batch/:batchId/eventsStream content import progress
SSE stream.
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/content-import/fetchFetch source content
Reads the sitemap or connected CMS and records what it finds against the batch.
Returns an SSE stream.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| source_type | string | Yes | sitemap or a connected CMS provider. |
| batch_id | string | No | Batch to fetch into. A new one is created when omitted. |
| sitemap_url | string | No | Sitemap URL for sitemap imports. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/content-import/importImport fetched content as articles
Returns an SSE stream.
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Fetched item IDs to import. |
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}AI Visibility
Read-only visibility, citation, prompt, competitor, alert, and run data.
/api/ai-visibility/overviewGet AI Visibility overview
Returns aggregate metrics, trend points, provider breakdowns, top prompts to fix, alerts, and suggestions.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. Defaults to the active product. |
| range | string | No | 7d (default), 14d, 30d, 90d, or custom. |
| window | string | No | Alias for range, kept for older clients. |
| date_from | string | No | Start date (YYYY-MM-DD). Used when range is custom. |
| date_to | string | No | End date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day. |
| provider | string | No | Narrow to one answer engine: chatgpt, gemini, perplexity, claude, or google_aio. All engines when omitted. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| product | Product | No | The product the numbers are for. |
| setup_required | boolean | No | True until AI Visibility has been configured for the product. |
| metrics | object | No | Headline numbers for the window: { visibilityScore, citationRate, shareOfVoice, allDetectedShareOfVoice, sentimentScore, prominenceIndex, sampleCount, promptObservationCount, confidence }. |
| previous_metrics | object | No | The same numbers for the preceding window, for comparison. |
| metrics_truncated | boolean | No | True when the window held more samples than were read. |
| by_provider | object | No | The same metrics keyed by engine: chatgpt, gemini, perplexity, claude, google_aio. |
| competitors | object[] | No | Competitor brands with their share of voice, visibility and citation rate. |
| trend_competitors | object[] | No | The competitors plotted in the trend series. |
| share_of_voice_trend | object[] | No | Share of voice per day, as { date, share_of_voice, sample_count }. |
| share_of_voice_trend_granularity | string | No | Whether the trend is bucketed by day or by week. |
| metric_trend | object[] | No | Every headline metric per day. |
| metric_trend_granularity | string | No | Whether the metric trend is bucketed by day or by week. |
| trend | object[] | No | Mention split per day, as { date, own, competitor, other, competitors }. |
| top_prompts_to_fix | object[] | No | Prompts where competitors lead, worst first. |
| prompts | object[] | No | The tracked prompts, as { id, prompt_text, intent_bucket, priority, active }. |
| alerts | object[] | No | Open alerts for the product. |
| insights | object[] | No | What changed in the window and why. |
| suggestions | object[] | No | Recommended next actions. |
| observation_days | number | No | Days in the window that actually held samples. |
| window_days | number | No | Length of the window in days. |
| date_range | object | No | The window the numbers cover, as { key, from, to, days, label }. |
| provider | string | Yes | The engine the response was narrowed to, or null for all engines. |
| billing_plan | string | No | The plan the account is on. |
| included_providers | string[] | No | Engines the plan covers. |
| locked_providers | string[] | No | Engines the plan does not cover. |
| provider_access | object[] | No | Per-engine availability and what unlocks it. |
| formulas | object | No | How each headline metric is calculated. |
/api/ai-visibility/settingsGet AI Visibility settings
Read the configured brand, competitors, aliases, providers, cadence, and setup state.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. Defaults to the active product. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| product | Product | No | The product the settings belong to. |
| settings | object | No | The configuration: { brand_name, brand_domain, brand_aliases, competitors, country, locale, persona, cadence, is_enabled, selected_providers }. |
| suggested_brand_domain | string | Yes | Domain suggested from the product website when none is set. |
| active_prompts_count | number | No | How many prompts are currently tracked. |
| active_prompt_limit | number | No | How many the plan allows. |
| generated_prompt_target | number | No | How many prompts setup aims to produce. |
| billing_plan | string | No | The plan the account is on. |
| included_providers | string[] | No | Engines the plan covers. |
| locked_providers | string[] | No | Engines the plan does not cover. |
| provider_access | object[] | No | Per-engine availability and what unlocks it. |
| selected_providers | string[] | No | Engines chosen in settings. |
| effective_selected_providers | string[] | No | Engines that will actually run, once the plan is applied. |
| can_manage_claude_addon | boolean | No | Whether the account can change the Claude add-on. |
/api/ai-visibility/promptsList tracked prompts
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| page | number | No | Page number. Default 1. |
| page_size | number | No | Page size. Max 100. |
| active | boolean | No | Set false to list inactive prompts. |
| include_metrics | boolean | No | Set false to skip per-prompt metrics and return the list faster. |
| include_clusters | boolean | No | Set true to include the cluster each prompt belongs to. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| prompts | object[] | No | One page of tracked prompts. |
| prompts[].id | string | No | Prompt ID. |
| prompts[].prompt_text | string | No | The question put to the engines. |
| prompts[].intent_bucket | string | Yes | The intent the prompt represents. |
| prompts[].priority | number | Yes | How important the prompt is. |
| prompts[].active | boolean | No | Whether the prompt is still being run. |
| prompts[].position | number | Yes | Display order. |
| prompts[].source | string | Yes | How the prompt was created. |
| prompts[].last_run_at | string | Yes | When the prompt last ran. |
| prompts[].metrics | object | Yes | Per-prompt metrics, unless include_metrics=false. |
| prompts[].top_competitors | object[] | Yes | The brands that win this prompt. |
| competitor_suggestions | object[] | No | Brands seen in answers that are not yet tracked as competitors. |
| metrics_truncated | boolean | No | True when more samples existed than were read. |
| total | number | No | Total rows across all pages. |
| page | number | No | The page returned. |
| page_size | number | No | Rows per page. |
/api/ai-visibility/prompts/:idGet prompt detail
Returns prompt metrics, provider responses, citations, fan-out queries, competitor rows, and timeline data.
| Name | Type | Required | Description |
|---|---|---|---|
| page | number | No | Response page number. |
| page_size | number | No | Response page size. Max 50. |
| citation_page | number | No | Citation page number. |
| citation_page_size | number | No | Citation page size. Max 50. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| prompt | object | No | The prompt, as listed. |
| metrics | object | No | Metrics for this prompt: { visibilityScore, citationRate, shareOfVoice, allDetectedShareOfVoice, sentimentScore, prominenceIndex, sampleCount, promptObservationCount, confidence }. |
| responses | object | No | A page of engine answers, as { data, total, page, page_size }. |
| citations | object | No | A page of the URLs those answers cited. |
| fanouts | object[] | Yes | The follow-up queries engines ran. |
| competitors | object[] | No | Brands mentioned alongside yours in this prompt. |
| timeline | object[] | No | How this prompt has moved over time. |
/api/ai-visibility/citationsList AI Visibility citations
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| provider | string | No | Narrow to one answer engine: chatgpt, gemini, perplexity, claude, or google_aio. All engines when omitted. |
| category | string | No | Citation category filter. |
| prompt_id | string | No | Prompt filter. |
| search | string | No | Case-insensitive title, domain, or URL search. Max 200 characters. |
| page | number | No | Page number. Default 1. |
| page_size | number | No | Page size. Max 100. |
| range | string | No | 7d (default), 14d, 30d, 90d, or custom. |
| window | string | No | Alias for range, kept for older clients. |
| date_from | string | No | Start date (YYYY-MM-DD). Used when range is custom. |
| date_to | string | No | End date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day. |
| view | string | No | all or own. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| citations | object[] | No | One page of cited URLs. |
| citations[].id | string | No | Citation ID. |
| citations[].url | string | No | The URL that was cited. |
| citations[].normalized_url | string | No | The URL with tracking and casing removed. |
| citations[].domain | string | No | Host of the cited URL. |
| citations[].title | string | Yes | Page title. |
| citations[].description | string | Yes | Page description. |
| citations[].provider | string | No | The engine that cited it. |
| citations[].is_own_domain | boolean | No | Whether the URL is on your own site. |
| citations[].position | number | Yes | Where it appeared in the answer. |
| citations[].source_type | string | Yes | What kind of source it is. |
| citations[].first_seen_at | string | No | When it was first cited. |
| citations[].prompt_id | string | No | The prompt whose answer cited it. |
| websites | object[] | No | The cited URLs rolled up per page, with content category and citation count. |
| category_summaries | object[] | No | Counts per content category. |
| domains | object[] | No | Counts per domain, as { domain, citation_count, website_count, own }. |
| total_citations | number | No | Citations matching the filters. |
| total_all_citations | number | No | Citations in the window before filters. |
| total_websites | number | No | Distinct pages matching the filters. |
| total_all_websites | number | No | Distinct pages in the window before filters. |
| truncated | boolean | No | True when more citations existed than were read. |
| date_range | object | No | The window the numbers cover, as { key, from, to, days, label }. |
| total | number | No | Total rows across all pages. |
| page | number | No | The page returned. |
| page_size | number | No | Rows per page. |
/api/ai-visibility/competitorsList competitor profiles
Returns lightweight competitor profile summaries with AI visibility, share of voice, citation rate, citation URL count, and stable identifiers.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| competitor | string | No | Stable competitor identifier or legacy name used to scope gap_prompts. |
| range | string | No | 7d (default), 14d, 30d, 90d, or custom. |
| window | string | No | Alias for range, kept for older clients. |
| date_from | string | No | Start date (YYYY-MM-DD). Used when range is custom. |
| date_to | string | No | End date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| competitors | object[] | No | The competitors you track, with share of voice, visibility, citation rate, prominence and sentiment. |
| field_brands | object[] | No | Every brand seen in answers, tracked or not, with the same numbers. |
| competitor_profiles | object[] | No | Profile summaries, as { identifier, brand_name, domain, aliases, visibility_score, share_of_voice, citation_rate, mentions, sample_count, citation_url_count }. Use identifier as the path value on the detail endpoint. |
| own | object | No | Your own numbers, for comparison: { visibilityScore, citationRate, shareOfVoice, allDetectedShareOfVoice, sentimentScore, prominenceIndex, sampleCount, promptObservationCount, confidence }. |
| gap_prompts | object[] | No | Prompts where a competitor leads you, widest gap first. |
| selected_competitor | string | Yes | The competitor gap_prompts was scoped to. |
| metrics_truncated | boolean | No | True when more samples existed than were read. |
| total | number | No | How many competitors were returned. |
| window_days | number | No | Length of the window in days. |
| date_range | object | No | The window the numbers cover, as { key, from, to, days, label }. |
/api/ai-visibility/competitors/:competitorGet competitor profile
Returns one competitor profile and a paginated list of citation URLs. Each URL carries its content category and a per-prompt breakdown of where it was cited. The competitor path value is the stable identifier returned by the list endpoint; legacy encoded names are also accepted.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| page | number | No | Citation URL page number. Default 1. Clamped to the last page of the filtered set. |
| page_size | number | No | Citation URL page size. Max 100. |
| citation_category | string | No | Filter citation URLs by content category (listicle, marketing_page, comparison, …). |
| citation_provider | string | No | Filter citation URLs by AI engine. |
| citation_prompt | string | No | Filter citation URLs to those cited under one prompt id. |
| citation_search | string | No | Free-text match against citation URL and title. Max 200 chars. |
| range | string | No | 7d (default), 14d, 30d, 90d, or custom. |
| window | string | No | Alias for range, kept for older clients. |
| date_from | string | No | Start date (YYYY-MM-DD). Used when range is custom. |
| date_to | string | No | End date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| competitor | object | No | The competitor profile. |
| citation_urls | object[] | No | A page of URLs cited for this competitor, each with its content category and a per-prompt breakdown. |
| total | number | No | Citation URLs matching the filters. |
| page | number | No | The page returned, clamped to the last page of the filtered set. |
| page_size | number | No | Rows per page. |
| date_range | object | No | The window the numbers cover, as { key, from, to, days, label }. |
/api/ai-visibility/alertsList AI Visibility alerts
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| status | string | No | Alert status filter. |
| page | number | No | Page number. |
| page_size | number | No | Page size. Max 100. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| alerts | object[] | No | One page of alerts, newest first. |
| alerts[].id | string | No | Alert ID. |
| alerts[].alert_type | string | No | What kind of change was detected. |
| alerts[].severity | string | No | How serious it is. |
| alerts[].title | string | No | One-line summary. |
| alerts[].message | string | No | The detail behind the alert. |
| alerts[].status | string | No | Whether the alert is still open. |
| alerts[].metric_key | string | Yes | The metric that moved. |
| alerts[].metric_delta | number | Yes | How far it moved. |
| alerts[].provider | string | Yes | The engine involved. |
| alerts[].prompt_id | string | Yes | The prompt involved. |
| alerts[].detected_at | string | No | When it was detected. |
| total | number | No | Total rows across all pages. |
| page | number | No | The page returned. |
| page_size | number | No | Rows per page. |
/api/ai-visibility/suggestionsList AI Visibility suggestions
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| status | string | No | Suggestion status filter. |
| page | number | No | Page number. |
| page_size | number | No | Page size. Max 100. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| product | object | No | The product, as { id, name }. |
| suggestions | object[] | No | One page of recommended actions. |
| suggestions[].id | string | No | Suggestion ID. |
| suggestions[].title | string | No | What to do. |
| suggestions[].body | string | No | Why it is worth doing. |
| suggestions[].action_type | string | No | The kind of action it asks for. |
| suggestions[].category | string | Yes | What area it covers. |
| suggestions[].urgency | string | Yes | How soon it matters. |
| suggestions[].effort | string | Yes | Rough effort involved. |
| suggestions[].week_start | string | Yes | The week the suggestion belongs to. |
| total | number | No | Total rows across all pages. |
| page | number | No | The page returned. |
| page_size | number | No | Rows per page. |
/api/ai-visibility/runsList AI Visibility runs
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| page | number | No | Page number. |
| page_size | number | No | Page size. Max 100. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| runs | object[] | No | One page of runs, newest first. |
| runs[].id | string | No | Run ID. |
| runs[].run_kind | string | No | What triggered the run. |
| runs[].run_date | string | No | The day the run covers. |
| runs[].status | string | No | Where the run got to. |
| runs[].country | string | Yes | Country the prompts were asked from. |
| runs[].prompts_count | number | No | Prompts included in the run. |
| runs[].samples_expected | number | No | Answers the run set out to collect. |
| runs[].samples_completed | number | No | Answers it actually collected. |
| runs[].provider_state | object | Yes | Per-engine outcome for the run. |
| runs[].error | string | Yes | Why the run failed. |
| runs[].started_at | string | Yes | When the run began. |
| runs[].completed_at | string | Yes | When it finished. |
| total | number | No | Total rows across all pages. |
| page | number | No | The page returned. |
| page_size | number | No | Rows per page. |
SEO Audits
Create, inspect, and process technical SEO audits.
/api/seo-auditList audits
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID filter. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| [] | Audit[] | No | Audits for the product, newest first. |
| [].id | string | No | Audit ID. |
| [].product_id | string | Yes | Product the audit belongs to. |
| [].site_url | string | No | The site that was audited. |
| [].site_label | string | Yes | Display name for the site. |
| [].status | string | No | Where the audit has got to. |
| [].total_pages | number | No | Pages the audit set out to cover. |
| [].crawled_pages | number | No | Pages fetched so far. |
| [].analyzed_pages | number | No | Pages scored so far. |
| [].summary | object | Yes | The written summary, with overallScore, topIssues, quickWins and issue counts. |
| [].summary_status | string | Yes | Whether the summary is ready. |
| [].summary_history | array | Yes | Previous summaries, newest first. |
| [].options | object | Yes | How the audit was configured: { mode, auditSource, skipLighthouse }. |
| [].link_graph | object | Yes | How the site links to itself. |
| [].link_analysis | object | Yes | What the link analysis found. |
| [].link_analysis_status | string | Yes | Whether link analysis is ready. |
| [].linking_report | object | Yes | Internal linking recommendations. |
| [].link_health_status | string | Yes | Whether the broken-link check is ready. |
| [].link_health_summary | object | Yes | Broken and redirecting links found. |
| [].link_health_completed_at | string | Yes | When the link check finished. |
| [].lighthouse_status | string | Yes | Whether Lighthouse has run. |
| [].lighthouse_attempted_pages | number | Yes | Pages Lighthouse tried. |
| [].lighthouse_completed_pages | number | Yes | Pages Lighthouse finished. |
| [].lighthouse_completed_at | string | Yes | When Lighthouse finished. |
| [].created_at | string | No | When the audit was created. |
| [].updated_at | string | No | When it last changed. |
/api/seo-auditCreate an audit
Creates an audit job and auto-starts background processing when possible.
Background job. Read GET /api/seo-audit/:id for status and page counts.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| sitemap_url | string | No | Website or sitemap URL. |
| urls | string[] | No | Explicit URLs to audit. |
| mode | string | No | full or lite. |
| skip_lighthouse | boolean | No | Skip Lighthouse analysis. |
| audit_source | string | No | product to audit the product website, url to audit sitemap_url on its own. Defaults to url when no product is given. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| Audit | No | The new audit, shaped as in the list above, returned with HTTP 201. Poll GET /api/seo-audit/:id for progress. | |
| product_name | string | Yes | Name of the product the audit belongs to. |
| page_count | number | No | How many URLs the audit will cover. |
| discovery_source | string | No | How those URLs were found. |
/api/seo-audit/:idGet audit details
| Name | Type | Required | Description |
|---|---|---|---|
| slim | boolean | No | Set to 1 for lightweight polling. |
| no_pages | boolean | No | Set to 1 to return audit metadata without page rows. |
| include_report_data | boolean | No | Set to 1 to include the full report payload. |
| page_limit | number | No | Number of page rows to return. |
| page_offset | number | No | Offset into the page rows. |
| status | string | No | Filter page rows by crawl status. Default all. |
| score | string | No | Filter page rows by score band. Default all. |
| q | string | No | Search page rows by URL or title. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Audit ID. |
| product_id | string | Yes | Product the audit belongs to. |
| site_url | string | No | The site that was audited. |
| site_label | string | Yes | Display name for the site. |
| status | string | No | Where the audit has got to. |
| total_pages | number | No | Pages the audit set out to cover. |
| crawled_pages | number | No | Pages fetched so far. |
| analyzed_pages | number | No | Pages scored so far. |
| summary | object | Yes | The written summary, with overallScore, topIssues, quickWins and issue counts. |
| summary_status | string | Yes | Whether the summary is ready. |
| summary_history | array | Yes | Previous summaries, newest first. |
| options | object | Yes | How the audit was configured: { mode, auditSource, skipLighthouse }. |
| link_graph | object | Yes | How the site links to itself. |
| link_analysis | object | Yes | What the link analysis found. |
| link_analysis_status | string | Yes | Whether link analysis is ready. |
| linking_report | object | Yes | Internal linking recommendations. |
| link_health_status | string | Yes | Whether the broken-link check is ready. |
| link_health_summary | object | Yes | Broken and redirecting links found. |
| link_health_completed_at | string | Yes | When the link check finished. |
| lighthouse_status | string | Yes | Whether Lighthouse has run. |
| lighthouse_attempted_pages | number | Yes | Pages Lighthouse tried. |
| lighthouse_completed_pages | number | Yes | Pages Lighthouse finished. |
| lighthouse_completed_at | string | Yes | When Lighthouse finished. |
| created_at | string | No | When the audit was created. |
| updated_at | string | No | When it last changed. |
| pages | object[] | No | The crawled pages, with their scores and issues. Empty when no_pages=1. |
| page_counts | object | No | Page totals per crawl status. |
| page_titles | object | Yes | Page titles keyed by page id. |
| crawlRuntime | object | Yes | Live progress: { phase, currentUrl, processedUrls, analyzedUrls, totalUrls, failedUrls, discardedUrls, crawlTargetSeconds, crawlEtaSeconds, analysisEtaSeconds, summaryEtaSeconds, overallEtaSeconds }. |
/api/seo-audit/:idDelete an audit
Deletes the audit and its crawled pages.
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the audit and its pages were deleted. |
200 OK — JSON
{ "success": true }/api/seo-audit/:id/processStart or continue audit processing
Safe to call repeatedly. Each call continues the audit where it left off and returns the saved progress, hasMore, and time estimates.
Returns 409 with { paused: true } while paused. Poll the audit details endpoint for completion.
| Field | Type | Nullable | Description |
|---|---|---|---|
| progress | object | No | Counts so far, as { total, crawled, analyzed }. |
| hasMore | boolean | No | Whether work remains. Call again while true. |
| pages | object[] | No | Pages finished by this call. |
| crawlRuntime | object | Yes | Live progress and time estimates, as on the audit detail endpoint. |
| paused | boolean | Yes | Returned with HTTP 409 when the audit is paused. |
200 OK — JSON
{
"progress": { "total": 160, "crawled": 42, "analyzed": 37 },
"hasMore": true,
"pages": [],
"crawlRuntime": {
"phase": "crawling",
"currentUrl": "https://example.com/docs",
"processedUrls": 42,
"analyzedUrls": 37,
"totalUrls": 160,
"failedUrls": 1,
"discardedUrls": 0,
"crawlTargetSeconds": 1080,
"crawlEtaSeconds": 708,
"analysisEtaSeconds": 738,
"summaryEtaSeconds": 60,
"overallEtaSeconds": 798
}
}/api/seo-audit/:id/summaryGet or queue the audit summary
Completed audits return the saved summary immediately. Audits still in progress continue where they left off and return the saved progress instead.
Clients must handle 200, 202, and 409 responses; an active request does not synchronously generate a summary.
| Field | Type | Nullable | Description |
|---|---|---|---|
| summary | object | Yes | The finished summary, on a completed audit. |
| linkGraph | object | Yes | The link graph that goes with it. |
| queued | boolean | Yes | Returned with HTTP 202 when the audit is still running. |
| progress | object | Yes | Counts so far, alongside queued. |
| hasMore | boolean | Yes | Whether work remains, alongside queued. |
| paused | boolean | Yes | Returned with HTTP 409 when the audit is paused. |
202 Accepted — JSON
// 200 — completed
{ "summary": { "overallScore": 82 }, "linkGraph": { "stats": { "totalPages": 160 } } }
// 202 — active
{ "queued": true, "progress": { "total": 160, "crawled": 42, "analyzed": 37 }, "hasMore": true }
// 409 — paused
{ "paused": true }
// 409 — failed
{ "error": "Resume, restart, or run a Deep Scan before requesting the summary." }/api/seo-audit/:id/link-analysisRun link analysis
Returns an SSE stream with progress.
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/seo-audit/:id/insightsGet audit insights
Returns the grouped issues found across the audited pages and how page scores are distributed.
copy_status is pending or generating while the written explanations are still being prepared, then ready. The rest of the response is already usable in all three states.
| Field | Type | Nullable | Description |
|---|---|---|---|
| insights | object | No | The findings: { diagnosis, stats, priorityActions, pageOpportunities, metadataQueue, performancePriorities }. |
| insights.diagnosis | string | No | What is holding the site back, in a sentence. |
| insights.stats | object | No | { analyzedPages, averageScore, criticalPages, quickWins }. |
| insights.priorityActions | object[] | No | What to fix first, each with title, problem, fix, impact, effort, scope and affected pages. |
| insights.pageOpportunities | object[] | No | Pages worth the most attention. |
| insights.metadataQueue | object[] | No | Titles and descriptions worth rewriting. |
| insights.performancePriorities | object[] | No | Speed problems worth fixing. |
| score_distribution | object | No | How page scores fall, as { good, fair, poor, total }. |
| copy_status | string | No | pending or generating while the written explanations are being prepared, then ready. |
200 OK — JSON
{
"insights": {
"diagnosis": "Thin metadata is the biggest drag on this site.",
"stats": { "analyzedPages": 160, "averageScore": 74, "criticalPages": 12, "quickWins": 23 },
"priorityActions": [
{ "id": "meta-descriptions", "title": "Write missing meta descriptions", "impact": "high", "effort": "low", "scope": 12, "pages": [] }
],
"pageOpportunities": [],
"metadataQueue": [],
"performancePriorities": []
},
"score_distribution": { "good": 84, "fair": 52, "poor": 24, "total": 160 },
"copy_status": "ready"
}/api/seo-audit/:id/page/:pageIdGet audited page details
Returns one crawled page with its checks and scores, plus a summary of the audit it belongs to.
| Name | Type | Required | Description |
|---|---|---|---|
| view | string | No | full (default) returns everything. Narrow the payload with summary, metadata, lighthouse, deep-analysis, or prompt-response. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Page ID. |
| url | string | No | The page URL. |
| status | string | No | Where the page got to in the crawl. |
| score | number | Yes | The page score. |
| analysis | object | Yes | The checks that were run and what they found. |
| page_summary | object | Yes | A short summary of the page. |
| crawl_data | object | Yes | Metadata and structure read from the page. The raw body is never included. |
| lighthouse_data | object | Yes | Lighthouse results, when Lighthouse ran. |
| deep_analysis | object | Yes | Deep analysis of the page, when one has been run. |
| error | string | Yes | Why the page could not be processed. |
| audit | object | No | The audit the page belongs to. |
/api/seo-audit/:id/page/:pageId/deep-analysisRun deep page analysis
Returns an SSE stream with progress.
200 OK — text/event-stream
event: progress
data: {"status":"running","processed":12,"total":50}
event: done
data: {"status":"completed","id":"..."}/api/seo-audit/preview-sitemapPreview sitemap URLs
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| urls | string[] | No | The URLs the audit would cover. |
| sitemap_url | string | Yes | The sitemap the URLs came from. |
| total | number | No | How many URLs were found. |
| limited | boolean | No | True when the list was cut to the plan limit. |
| limit | number | No | The most URLs your plan allows in one audit. |
Backlinks
Manage backlink sessions, research referring domains, and enrich opportunities.
/api/backlinksList backlink sessions
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| [] | BacklinkSession[] | No | Sessions for the product, newest first. |
| [].id | string | No | Session ID. |
| [].name | string | No | Session name. |
| [].product_id | string | No | Product the session belongs to. |
| [].created_at | string | No | When the session was created. |
/api/backlinksCreate backlink session
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Session name. |
| product_id | string | Yes | Product ID. |
| csv_content | string | Yes | CSV content. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| BacklinkSession | No | The new session, returned with HTTP 201, shaped as in the session list. |
/api/backlinks/:idGet backlink session
Returns the session with its opportunities.
| Name | Type | Required | Description |
|---|---|---|---|
| counts_only | boolean | No | Set true to return row counts instead of the opportunities. |
| discarded | string | No | true for discarded opportunities only, false to exclude them. Both when omitted. |
| classification | string | No | Comma-separated opportunity classifications. |
| enrichment_status | string | No | Filter by enrichment state. |
| outreach_status | string | No | Comma-separated outreach states. |
| site_category | string | No | Comma-separated site categories. |
| site_type | string | No | Comma-separated site types. |
| page_content_type | string | No | Comma-separated page content types. |
| search | string | No | Case-insensitive match against the referring domain. |
| min_score | number | No | Lowest total score to include. |
| max_score | number | No | Highest total score to include. |
| sort | string | No | Field to sort by. Default total_score. |
| order | string | No | asc or desc. Default desc. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | No | Session ID. |
| name | string | No | Session name. |
| product_name | string | Yes | Name of the product the session belongs to. |
| opportunities | object[] | No | The matching opportunities, sorted by the sort and order parameters. |
| opportunities[].id | string | No | Opportunity ID. |
| opportunities[].referring_domain | string | No | The domain that could link to you. |
| opportunities[].total_score | number | Yes | How promising the opportunity is. |
| opportunities[].classification | string | Yes | What kind of opportunity it is. |
| opportunities[].enrichment_status | string | Yes | Whether contact details have been looked up. |
| opportunities[].outreach_status | string | Yes | Where the opportunity has got to in outreach. |
| opportunities[].discarded | boolean | No | Whether the opportunity has been set aside. |
| counts | object | Yes | Returned instead of opportunities when counts_only=true. |
/api/backlinks/:idDelete backlink session
| Field | Type | Nullable | Description |
|---|---|---|---|
| success | boolean | No | True when the session was deleted. |
200 OK — JSON
{ "success": true }/api/backlinks/:id/enrichEnrich backlink opportunities
Returns an NDJSON stream with progress updates.
| Name | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Opportunity IDs. |
200 OK — application/x-ndjson
{"type":"progress","processed":12,"total":50}
{"type":"result","id":"...","status":"updated"}
{"type":"done","processed":50}/api/backlinks/researchRun backlink research
| Name | Type | Required | Description |
|---|---|---|---|
| target | string | Yes | Domain or URL. |
| query_type | string | No | backlinks, referring_domains, competitors, or domain_intersection. |
| product_id | string | No | Product ID. |
| limit | number | No | Result limit. |
| competitors | string[] | No | Up to four competitor domains, used by domain_intersection. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| query_id | string | No | The saved query. Read it back with GET /api/backlinks/research/:queryId. |
| target | string | No | The domain the research ran against. |
| query_type | string | No | The kind of research that ran. |
| total_count | number | No | How many rows exist for the target. |
| results | object[] | No | The rows found, strongest referring domain first, shaped as in the query detail response. |
/api/backlinks/research/historyList backlink research history
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID filter. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| queries | object[] | No | Past research queries, newest first. |
| queries[].id | string | No | Query ID. |
| queries[].target | string | No | The domain or URL that was researched. |
| queries[].query_type | string | No | backlinks, referring_domains, competitors, or domain_intersection. |
| queries[].targets | array | Yes | The competitor domains, for domain_intersection. |
| queries[].total_results | number | Yes | How many rows the query found. |
| queries[].created_at | string | No | When the research ran. |
| queries[].product | object | Yes | The product, as { id, name }. |
/api/backlinks/research/:queryIdGet backlink research results
Returns the saved query and its result rows, strongest referring domain first.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID filter. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| query | object | No | The saved query, as listed in the research history. |
| results | object[] | No | Its rows, strongest referring domain first. |
| results[].id | string | No | Result ID, used to create a session from it. |
| results[].domain_from | string | Yes | The linking domain. |
| results[].url_from | string | Yes | The linking page. |
| results[].domain_from_rank | number | Yes | Strength of the linking domain. |
| results[].page_from_rank | number | Yes | Strength of the linking page. |
| results[].anchor | string | Yes | The anchor text used. |
| results[].is_nofollow | boolean | Yes | Whether the link is nofollow. |
| results[].is_broken | boolean | Yes | Whether the link is broken. |
| results[].first_seen | string | Yes | When the link was first seen. |
| results[].last_seen | string | Yes | When it was last seen. |
200 OK — JSON
{
"query": { "id": "query-id", "target": "example.com", "query_type": "referring_domains" },
"results": [{ "id": "result-id", "domain_from": "blog.example.org", "domain_from_rank": 62 }]
}/api/backlinks/research/create-sessionCreate backlink session from research
| Name | Type | Required | Description |
|---|---|---|---|
| result_ids | string[] | Yes | Research result IDs. |
| product_id | string | Yes | Product ID. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| session_id | string | No | The session created from the chosen results. |
| session_name | string | No | That session's name. |
| opportunities_created | number | No | How many opportunities it holds. |
Search Console
Read cached Google Search Console performance data.
/api/gsc/connectionsList Search Console connections
Returns the Search Console properties connected to a product.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | No | Product ID. Defaults to the active product. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| [] | Connection[] | No | The Search Console properties connected to the product. |
| [].id | string | No | Connection ID. |
| [].product_id | string | No | Product the connection belongs to. |
| [].site_url | string | No | The Search Console property, such as sc-domain:example.com. |
| [].permission_level | string | Yes | The access Google grants on that property. |
| [].is_active | boolean | No | Whether the connection is in use. |
| [].last_synced_at | string | Yes | When data was last pulled. |
| [].created_at | string | No | When the connection was made. |
| [].updated_at | string | No | When it last changed. |
200 OK — JSON
[
{
"id": "connection-id",
"product_id": "product-id",
"site_url": "sc-domain:example.com",
"permission_level": "siteOwner",
"is_active": true,
"last_synced_at": "2026-09-20T02:00:00.000Z"
}
]/api/gsc/performanceGet performance data
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| date_from | string | No | Start date. |
| date_to | string | No | End date. |
| article_id | string | No | Article filter. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| total_clicks | number | No | Clicks across the period. |
| total_impressions | number | No | Impressions across the period. |
| avg_ctr | number | No | Average click-through rate, 0 to 1. |
| avg_position | number | No | Average position across the period. |
| articles | object[] | No | Per-article performance. |
| articles[].page_url | string | No | The published URL. |
| articles[].clicks | number | No | Clicks for that page. |
| articles[].impressions | number | No | Impressions for that page. |
| articles[].ctr | number | No | Click-through rate, 0 to 1. |
| articles[].avg_position | number | No | Average position for that page. |
/api/gsc/top-queriesGet top search queries
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| date_from | string | No | Start date. |
| date_to | string | No | End date. |
| limit | number | No | Number of queries to return. Default 20. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| queries | object[] | No | The best-performing search queries, most clicks first. |
| queries[].query | string | No | What the row is for. |
| queries[].clicks | number | No | Clicks in the period. |
| queries[].impressions | number | No | Impressions in the period. |
| queries[].ctr | number | No | Click-through rate, 0 to 1. |
| queries[].position | number | No | Average position. |
200 OK — JSON
{
"queries": [
{ "query": "best crm for startups", "clicks": 84, "impressions": 2100, "ctr": 0.04, "position": 7.2 }
]
}/api/gsc/article-analyticsGet article analytics
Search performance for one published article, broken down by day, query, device, country, and search appearance.
| Name | Type | Required | Description |
|---|---|---|---|
| article_id | string | Yes | Article ID. |
| date_from | string | No | Start date. |
| date_to | string | No | End date. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| summary | object | No | Totals for the article, as { clicks, impressions, ctr, position }. |
| daily | object[] | No | Performance per day. |
| queries | object[] | No | The queries the article ranks for. |
| devices | object[] | No | Performance per device. |
| countries | object[] | No | Performance per country. |
| searchAppearances | object[] | No | Performance per search appearance type. |
200 OK — JSON
{
"summary": { "clicks": 312, "impressions": 9800, "ctr": 0.032, "position": 9.1 },
"daily": [{ "date": "2026-09-01", "clicks": 12, "impressions": 380, "ctr": 0.031, "position": 8.8 }],
"queries": [],
"devices": [],
"countries": [],
"searchAppearances": []
}/api/gsc/site-analyticsGet site analytics
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| date_from | string | No | Start date. |
| date_to | string | No | End date. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| daily | object[] | No | Site performance per day. |
| daily[].date | string | No | What the row is for. |
| daily[].clicks | number | No | Clicks in the period. |
| daily[].impressions | number | No | Impressions in the period. |
| daily[].ctr | number | No | Click-through rate, 0 to 1. |
| daily[].position | number | No | Average position. |
| searchAppearances | object[] | No | Performance per search appearance type. |
| searchAppearances[].searchAppearance | string | No | What the row is for. |
| searchAppearances[].clicks | number | No | Clicks in the period. |
| searchAppearances[].impressions | number | No | Impressions in the period. |
| searchAppearances[].ctr | number | No | Click-through rate, 0 to 1. |
| searchAppearances[].position | number | No | Average position. |
200 OK — JSON
{
"daily": [{ "date": "2026-09-01", "clicks": 140, "impressions": 5200, "ctr": 0.027, "position": 12.4 }],
"searchAppearances": []
}/api/gsc/pagesGet page-level performance
Returns 403 when the product has no Search Console connection.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| pages | object[] | No | Performance per page. |
| pages[].url | string | No | The page URL. |
| pages[].clicks | number | No | Clicks in the period. |
| pages[].impressions | number | No | Impressions in the period. |
200 OK — JSON
{
"pages": [
{ "page": "https://example.com/blog/best-crm", "clicks": 84, "impressions": 2100, "ctr": 0.04, "position": 7.2 }
]
}/api/gsc/agent-suggestionsGet Search Console suggestions
Suggested next actions derived from the product's Search Console data. Results are cached and reused between calls.
A cached read is free, but producing fresh suggestions spends AI credits from your plan.
| Name | Type | Required | Description |
|---|---|---|---|
| product_id | string | Yes | Product ID. |
| force | boolean | No | Set true to ignore the cache and produce fresh suggestions. |
| refresh_empty_no_data | boolean | No | Set true to retry a cached result that came back empty. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| suggestions | object[] | No | Recommended next actions drawn from the Search Console data. |
| generated_at | string | Yes | When the suggestions were produced. |
| cached | boolean | Yes | True when a previous result was reused rather than generating fresh ones. |