API reference

Build on Roman

Use Roman’s public API to create articles, manage content workflows, publish to your stack, and bring SEO performance data into your own tools.

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

ScopeUse
api:readRead documented resources and analytics. Also covers the read-only AI Visibility API.
api:writeCreate, update, delete, publish, and start documented non-AI operations.
ai:executeRun 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

TypeHow to handle it
200 OK - JSONParse the response body as JSON. List endpoints usually return a data array plus pagination or totals.
202 Accepted - JSONThe job was queued or started. Poll the matching detail/status endpoint, or consume the event stream listed in the endpoint note.
text/event-streamRead Server-Sent Events until a done, completed, failed, or error event. Reconnect only if the job is still non-terminal.
application/x-ndjsonProcess one JSON object per line. Treat a final done line as success and an error line as a terminal failure.
binary fileUse 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

StatusRetryWhat to do
400NoFix the request payload or query parameters before sending again.
401NoCheck that the Bearer token is present, active, and not revoked.
403NoGrant the required scope, use a dashboard session, or switch to a documented public endpoint.
404NoVerify the resource ID and product/account ownership.
409After state changesPoll the active job or wait until the resource reaches a valid state.
429YesBack off, honor Retry-After when present, and add jitter before retrying.
500YesRetry with exponential backoff. Log the method, path, status, and response body.
503YesRetry 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

StepWhat happens
1. ConfigureAdd your receiver URL and optional secret in the Custom Webhook settings.
2. TriggerPublish the article from Roman after the webhook destination has been saved.
3. SendRoman 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. ConfirmAny 2xx response is treated as success. Roman then marks the article as published and records webhook publish history.
5. FailIf 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

FieldDescription
article_idUnique Roman article ID.
article_typeai_visibility_answer for visibility answer articles; otherwise normal_article.
article_subtypeSaved Roman article subtype, such as listicle, comparison, or ai_visibility_answer. May be null.
titleHeadline when available, then meta title, then target keyword.
headlineArticle headline, or null when unset.
cover_image_urlCover image URL, or null when no cover image is set.
meta_titleSEO title, or null when unset.
meta_descriptionSEO description, or null when unset.
target_keywordPrimary keyword the article targets.
content_htmlFull 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_markdownThe same body as Markdown. Converted when the saved body is HTML, passed through when it is already Markdown.
statusArticle status at the time the webhook is triggered.
published_atISO 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

CaseResult
Test webhookThe settings page sends a small JSON test payload to your URL with the same secret header.
No URLThe publish endpoint returns WEBHOOK_NOT_CONFIGURED.
No contentRoman rejects the publish until the article has HTML content.
Receiver errorRoman returns WEBHOOK_FAILED for non-2xx responses and WEBHOOK_ERROR when the URL cannot be reached.

Articles

Create, update, generate, publish, and export articles.

GET/api/articles

List articles

api:read

Retrieve a paginated list of articles with optional filters.

Query Parameters
NameTypeRequiredDescription
pagenumberNoPage number, starting at 0.
limitnumberNoItems per page. Default 20.
searchstringNoCase-insensitive match against target keyword, search query, headline, meta title, meta description, and published URL. Max 200 characters.
statusesstringNoComma-separated status filter.
product_idsstringNoComma-separated product IDs.
sourcestringNoComma-separated values for how the article was created, such as content_import.
article_typestringNoseo 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_fromstringNoOnly articles created on or after this date. YYYY-MM-DD covers the whole day; a full ISO timestamp is used as given.
created_tostringNoOnly articles created on or before this date.
updated_fromstringNoOnly articles last changed on or after this date.
updated_tostringNoOnly articles last changed on or before this date.
has_published_urlbooleanNotrue 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_imagebooleanNotrue for articles with a cover image, false for those without.
min_word_rangenumberNoLowest target word count to include.
max_word_rangenumberNoHighest target word count to include.
keyword_import_batch_idstringNoOnly articles created from this keyword import batch.
content_import_batch_idstringNoOnly articles created from this content import batch.
refreshstringNoRefresh state filter: scheduled, due_soon, in_progress, or failed.
sort_bystringNocreated_at (default), updated_at, target_keyword, headline, status, next_refresh_at, or last_refreshed_at. An unknown field falls back to created_at.
sort_orderstringNoasc or desc. Default desc.
groupedbooleanNoSet true to return articles grouped by status instead of one flat page.
limit_per_groupnumberNoRows per group when grouped is true. Default 10, max 50.
include_metabooleanNoSet true to include filter metadata such as the available import batches.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
dataArticle[]NoOne page of matching articles, without their bodies.
data[].idstringNoArticle ID.
data[].product_idstringYesProduct the article belongs to.
data[].target_keywordstringNoPrimary keyword the article targets.
data[].search_querystringYesQuery used for research. Defaults to the target keyword.
data[].word_rangenumberNoTarget word count. 0 means the length is chosen at generation time.
data[].statusstringNodraft, researching, outline_ready, writing, optimizing, complete, or published.
data[].sourcestringYesHow the article was created, such as content_import.
data[].source_urlstringYesURL the article was imported from.
data[].article_typestringYeslisticle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer.
data[].headlinestringYesArticle headline.
data[].meta_titlestringYesSEO title.
data[].meta_descriptionstringYesSEO description.
data[].cover_image_urlstringYesCover image URL.
data[].published_urlstringYesLive URL, once one has been recorded.
data[].unpublished_changes_atstringYesWhen the article last changed after being published.
data[].auto_refresh_enabledbooleanNoWhether the article is enrolled in scheduled refreshes.
data[].refresh_statusstringYesidle, queued, refreshing, or failed.
data[].last_refreshed_atstringYesWhen the article was last refreshed.
data[].next_refresh_atstringYesWhen the next refresh is due.
data[].keyword_import_batch_idstringYesKeyword import batch the article came from.
data[].content_import_batch_idstringYesContent import batch the article came from.
data[].created_atstringNoISO 8601 creation time.
data[].updated_atstringNoISO 8601 time of the last change.
data[].productobjectYesThe product, as { id, name }.
data[].keyword_import_batchobjectYesThe keyword batch, as { id, name }.
data[].content_import_batchobjectYesThe content batch, as { id, name }.
totalnumberNoTotal matches across all pages.
hasMorebooleanNoWhether a further page exists.
groupsobject[]YesReturned instead of data when grouped=true. Each entry is { key, statuses, articles, total }.
metaobjectYesReturned when include_meta=true, listing the import batches available as filters.
POST/api/articles

Create an article

api:write

Create a draft article record. Use the generation endpoints to produce the outline or content.

Body Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
target_keywordstringYesTarget keyword.
search_querystringNoSearch query. Defaults to target_keyword.
custom_instructionsstringNoExtra writing instructions.
word_rangenumberNoTarget word count. Default 2000.
auto_word_rangebooleanNoSet true to let the length be chosen at generation time. Overrides word_range.
article_typestringNolisticle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer. Chosen automatically when omitted.
source_metadataobjectNoFree-form metadata describing where the article idea came from.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoArticle ID.
product_idstringYesProduct the article belongs to.
target_keywordstringNoPrimary keyword the article targets.
search_querystringYesQuery used for research. Defaults to the target keyword.
word_rangenumberNoTarget word count. 0 means the length is chosen at generation time.
statusstringNodraft, researching, outline_ready, writing, optimizing, complete, or published.
sourcestringYesHow the article was created, such as content_import.
source_urlstringYesURL the article was imported from.
article_typestringYeslisticle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer.
headlinestringYesArticle headline.
meta_titlestringYesSEO title.
meta_descriptionstringYesSEO description.
cover_image_urlstringYesCover image URL.
published_urlstringYesLive URL, once one has been recorded.
unpublished_changes_atstringYesWhen the article last changed after being published.
auto_refresh_enabledbooleanNoWhether the article is enrolled in scheduled refreshes.
refresh_statusstringYesidle, queued, refreshing, or failed.
last_refreshed_atstringYesWhen the article was last refreshed.
next_refresh_atstringYesWhen the next refresh is due.
keyword_import_batch_idstringYesKeyword import batch the article came from.
content_import_batch_idstringYesContent import batch the article came from.
created_atstringNoISO 8601 creation time.
updated_atstringNoISO 8601 time of the last change.
productobjectYesThe product, as { id, name }.
content_markdownstringNoThe body as Markdown, whichever format it is stored in. Omit with content_markdown=false.
blog_contentstringYesBody as written by generation.
blog_htmlstringYesBody as last saved in the editor. Takes precedence over blog_content.
optimized_contentstringYesBody after optimisation, used when blog_html is unset.
outlineobjectYesThe outline the article was written from.
outline_htmlstringYesThe outline rendered as HTML.
custom_instructionsstringYesBrief applied while writing.
keyword_rangesstringYesKeyword frequency targets used while writing.
research_dataarrayYesCompetitor research gathered for the article.
expert_feedbackstringYesReview feedback recorded against the outline.
cta_dataarrayYesCalls to action placed in the body.
link_suggestionsobjectYesInternal and external link suggestions, including which have been applied.
extracted_domainsarrayYesDomains referenced by the body.
linkedin_poststringYesGenerated LinkedIn post draft.
deep_analysisobjectYesMost recent deep analysis of the article.
source_metadataobjectYesWhere the article idea came from.
refresh_interval_daysnumberNoDays between scheduled refreshes.
refresh_errorstringYesWhy the last refresh failed.
refresh_historyarrayYesPast refreshes, newest first.
publish_historyarrayYesPast publishes, newest first.
cms_publish_metadataobjectYesIdentifiers returned by the CMS on the last publish.
cms_field_valuesobjectYesValues for the CMS integration's custom fields.
public_share_enabledbooleanNoWhether a public share link is active.
public_share_created_atstringYesWhen the share link was created.
public_share_updated_atstringYesWhen the share link was last changed.
productobjectYesThe full product, including its writing style.
Example Response

200 OK — JSON

{
  "id": "article-id",
  "target_keyword": "best crm for startups",
  "status": "draft"
}
PATCH/api/articles

Bulk update article status

api:write
Body Parameters
NameTypeRequiredDescription
idsstring[]YesArticle IDs to update.
statusstringYesNew status.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the update was applied.
updatednumberNoHow many articles changed status.
Example Response

200 OK — JSON

{ "success": true, "updated": 5 }
DELETE/api/articles

Bulk delete articles

api:write

Permanently delete multiple articles and their associated assets.

Body Parameters
NameTypeRequiredDescription
idsstring[]YesArticle IDs to delete.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the articles were deleted.
deletednumberNoHow many articles were deleted.
GET/api/articles/:id

Get an article

api:read

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.

Query Parameters
NameTypeRequiredDescription
content_markdownbooleanNoSet false to leave content_markdown out when you only need the stored fields.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoArticle ID.
product_idstringYesProduct the article belongs to.
target_keywordstringNoPrimary keyword the article targets.
search_querystringYesQuery used for research. Defaults to the target keyword.
word_rangenumberNoTarget word count. 0 means the length is chosen at generation time.
statusstringNodraft, researching, outline_ready, writing, optimizing, complete, or published.
sourcestringYesHow the article was created, such as content_import.
source_urlstringYesURL the article was imported from.
article_typestringYeslisticle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer.
headlinestringYesArticle headline.
meta_titlestringYesSEO title.
meta_descriptionstringYesSEO description.
cover_image_urlstringYesCover image URL.
published_urlstringYesLive URL, once one has been recorded.
unpublished_changes_atstringYesWhen the article last changed after being published.
auto_refresh_enabledbooleanNoWhether the article is enrolled in scheduled refreshes.
refresh_statusstringYesidle, queued, refreshing, or failed.
last_refreshed_atstringYesWhen the article was last refreshed.
next_refresh_atstringYesWhen the next refresh is due.
keyword_import_batch_idstringYesKeyword import batch the article came from.
content_import_batch_idstringYesContent import batch the article came from.
created_atstringNoISO 8601 creation time.
updated_atstringNoISO 8601 time of the last change.
productobjectYesThe product, as { id, name }.
content_markdownstringNoThe body as Markdown, whichever format it is stored in. Omit with content_markdown=false.
blog_contentstringYesBody as written by generation.
blog_htmlstringYesBody as last saved in the editor. Takes precedence over blog_content.
optimized_contentstringYesBody after optimisation, used when blog_html is unset.
outlineobjectYesThe outline the article was written from.
outline_htmlstringYesThe outline rendered as HTML.
custom_instructionsstringYesBrief applied while writing.
keyword_rangesstringYesKeyword frequency targets used while writing.
research_dataarrayYesCompetitor research gathered for the article.
expert_feedbackstringYesReview feedback recorded against the outline.
cta_dataarrayYesCalls to action placed in the body.
link_suggestionsobjectYesInternal and external link suggestions, including which have been applied.
extracted_domainsarrayYesDomains referenced by the body.
linkedin_poststringYesGenerated LinkedIn post draft.
deep_analysisobjectYesMost recent deep analysis of the article.
source_metadataobjectYesWhere the article idea came from.
refresh_interval_daysnumberNoDays between scheduled refreshes.
refresh_errorstringYesWhy the last refresh failed.
refresh_historyarrayYesPast refreshes, newest first.
publish_historyarrayYesPast publishes, newest first.
cms_publish_metadataobjectYesIdentifiers returned by the CMS on the last publish.
cms_field_valuesobjectYesValues for the CMS integration's custom fields.
public_share_enabledbooleanNoWhether a public share link is active.
public_share_created_atstringYesWhen the share link was created.
public_share_updated_atstringYesWhen the share link was last changed.
productobjectYesThe full product, including its writing style.
Example Response

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" }
}
PATCH/api/articles/:id

Update an article

api:write

Send only the fields you want to change.

Body Parameters
NameTypeRequiredDescription
target_keywordstringNoUpdated keyword.
statusstringNoUpdated status.
content_htmlstringNoUpdated HTML content.
outlineobjectNoUpdated outline.
headlinestringNoUpdated headline.
meta_titlestringNoUpdated SEO title.
meta_descriptionstringNoUpdated SEO description.
published_urlstringNoLive URL of the published article. Must be on the product website or a connected Search Console property.
source_metadataobjectNoMerged into the existing source metadata rather than replacing it.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoArticle ID.
product_idstringYesProduct the article belongs to.
target_keywordstringNoPrimary keyword the article targets.
search_querystringYesQuery used for research. Defaults to the target keyword.
word_rangenumberNoTarget word count. 0 means the length is chosen at generation time.
statusstringNodraft, researching, outline_ready, writing, optimizing, complete, or published.
sourcestringYesHow the article was created, such as content_import.
source_urlstringYesURL the article was imported from.
article_typestringYeslisticle, comparison, how_to_guide, review, explainer, general, or ai_visibility_answer.
headlinestringYesArticle headline.
meta_titlestringYesSEO title.
meta_descriptionstringYesSEO description.
cover_image_urlstringYesCover image URL.
published_urlstringYesLive URL, once one has been recorded.
unpublished_changes_atstringYesWhen the article last changed after being published.
auto_refresh_enabledbooleanNoWhether the article is enrolled in scheduled refreshes.
refresh_statusstringYesidle, queued, refreshing, or failed.
last_refreshed_atstringYesWhen the article was last refreshed.
next_refresh_atstringYesWhen the next refresh is due.
keyword_import_batch_idstringYesKeyword import batch the article came from.
content_import_batch_idstringYesContent import batch the article came from.
created_atstringNoISO 8601 creation time.
updated_atstringNoISO 8601 time of the last change.
productobjectYesThe product, as { id, name }.
content_markdownstringNoThe body as Markdown, whichever format it is stored in. Omit with content_markdown=false.
blog_contentstringYesBody as written by generation.
blog_htmlstringYesBody as last saved in the editor. Takes precedence over blog_content.
optimized_contentstringYesBody after optimisation, used when blog_html is unset.
outlineobjectYesThe outline the article was written from.
outline_htmlstringYesThe outline rendered as HTML.
custom_instructionsstringYesBrief applied while writing.
keyword_rangesstringYesKeyword frequency targets used while writing.
research_dataarrayYesCompetitor research gathered for the article.
expert_feedbackstringYesReview feedback recorded against the outline.
cta_dataarrayYesCalls to action placed in the body.
link_suggestionsobjectYesInternal and external link suggestions, including which have been applied.
extracted_domainsarrayYesDomains referenced by the body.
linkedin_poststringYesGenerated LinkedIn post draft.
deep_analysisobjectYesMost recent deep analysis of the article.
source_metadataobjectYesWhere the article idea came from.
refresh_interval_daysnumberNoDays between scheduled refreshes.
refresh_errorstringYesWhy the last refresh failed.
refresh_historyarrayYesPast refreshes, newest first.
publish_historyarrayYesPast publishes, newest first.
cms_publish_metadataobjectYesIdentifiers returned by the CMS on the last publish.
cms_field_valuesobjectYesValues for the CMS integration's custom fields.
public_share_enabledbooleanNoWhether a public share link is active.
public_share_created_atstringYesWhen the share link was created.
public_share_updated_atstringYesWhen the share link was last changed.
productobjectYesThe full product, including its writing style.
DELETE/api/articles/:id

Delete an article

api:write
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the article was deleted.
Example Response

200 OK — JSON

{ "success": true }
POST/api/articles/:id/generate-outline

Generate an outline

api:writeai:execute

Starts outline generation and returns 202 Accepted.

Fire-and-forget. Poll the article status or consume generation-events until a terminal event.

Body Parameters
NameTypeRequiredDescription
streambooleanNoWhen true, response includes a progress stream URL.
Response Fields — 202 Accepted — JSON
FieldTypeNullableDescription
acceptedbooleanNoTrue once the run has been queued.
articleIdstringNoThe article the run belongs to.
streamobjectYesPresent when stream=true, as { runId, url } pointing at the generation-events stream.
Example Response

202 Accepted — JSON

{
  "accepted": true,
  "articleId": "...",
  "stream": { "runId": "...", "url": "/api/articles/.../generation-events?runId=..." }
}
POST/api/articles/:id/generate-content

Generate article content

api:writeai:execute

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.

Body Parameters
NameTypeRequiredDescription
keyword_rangesobjectNoSEO keyword ranges.
custom_instructionsstringNoArticle brief used while writing. Saved on the article; send an empty string to clear it.
streambooleanNoWhen true, response includes a progress stream URL.
Response Fields — 202 Accepted — JSON
FieldTypeNullableDescription
acceptedbooleanNoTrue once the run has been queued.
articleIdstringNoThe article the run belongs to.
streamobjectYesPresent when stream=true, as { runId, url } pointing at the generation-events stream.
POST/api/articles/:id/generate-article

Generate full article

api:writeai:execute

Starts outline and content generation in one background run.

Fire-and-forget. Use generation-events for live progress.

Body Parameters
NameTypeRequiredDescription
streambooleanNoWhen true, response includes a progress stream URL.
Response Fields — 202 Accepted — JSON
FieldTypeNullableDescription
acceptedbooleanNoTrue once the run has been queued.
articleIdstringNoThe article the run belongs to.
streamobjectYesPresent when stream=true, as { runId, url } pointing at the generation-events stream.
GET/api/articles/:id/generation-events

Stream article generation progress

api:read

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.

Query Parameters
NameTypeRequiredDescription
runIdstringNoGeneration run ID returned by the start endpoint.
kindstringNooutline or content.
Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
POST/api/articles/:id/generate-links

Generate link suggestions

api:writeai:execute

Return internal and external link suggestions for an article.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
link_suggestionsobjectNoInternal and external link suggestions, each with its anchor text and target URL. Also saved on the article.
POST/api/articles/:id/generate-linkedin-post

Generate LinkedIn post draft

api:writeai:execute
Body Parameters
NameTypeRequiredDescription
customVisionstringNoExtra instructions for the post.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
ArticleNoThe updated article, shaped exactly like GET /api/articles/:id. The draft is in linkedin_post.
POST/api/articles/:id/publish-cms

Publish to connected CMS

api:write

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.

Body Parameters
NameTypeRequiredDescription
contentHtmlstringNoHTML to publish instead of the saved article body.
contentMarkdownstringNoMarkdown to publish, for CMS integrations that take Markdown.
headlinestringNoTitle to publish under.
metaTitlestringNoSEO title. Used as the publish title when headline is absent.
metaDescriptionstringNoSEO description. Send an empty string to publish without one.
imageUrlstringNoCover image URL. Send an empty string to publish without one.
cmsFieldValuesobjectNoValues for the custom fields configured on the CMS integration.
Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
POST/api/articles/:id/sync-live

Sync from the live page

api:writeai:execute

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.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
articleArticleNoThe article after the sync, shaped exactly like GET /api/articles/:id.
sync.synced_atstringNoWhen the article last changed to match the published version.
sync.checked_atstringNoWhen the published version was read this time.
sync.urlstringNoThe address the article was read from.
sync.viastringNocms 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.triggerstringNomanual for this endpoint; auto for syncs the product's Live Sync schedule ran.
sync.word_countnumberNoWords in the synced article.
sync.changedbooleanNoFalse when the published version already matched, so nothing was written.
sync.flagsstring[]NoWarnings that part of the page may be missing. Check the article before publishing it when this is not empty.
sync.removed_blocksnumberNoParts of the page that were not the article and were left out.
sync.revisionnumberYesThe history entry holding the version before this sync, or null when nothing changed.
POST/api/articles/:id/publish-webhook

Publish to your webhook

api:write

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.

Body Parameters
NameTypeRequiredDescription
contentHtmlstringNoHTML to send instead of the saved article body. content_markdown is derived from it.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when your receiver accepted the article.
messagestringNoHuman-readable confirmation.
webhook_statusnumberNoThe HTTP status your receiver returned.
Example Response

200 OK — JSON

{
  "success": true,
  "message": "Article published via webhook successfully.",
  "webhook_status": 200
}
POST/api/articles/:id/export-docx

Export as DOCX

api:write

Returns a binary DOCX file.

Body Parameters
NameTypeRequiredDescription
htmlstringYesHTML content to export.
titlestringNoDocument title.
Success Response

200 OK — file download

A binary response body. Read it as a blob or a stream; do not parse it as JSON.
GET/api/articles/:id/assets

List article assets

api:read
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
assetsobject[]NoAssets attached to the article.
assets[].idstringNoAsset ID.
assets[].typestringNoAsset kind, such as image or pdf.
assets[].statusstringNopending, approved, or rejected.
PATCH/api/articles/:id/assets

Update asset status

api:write
Body Parameters
NameTypeRequiredDescription
assetIdstringYesAsset ID.
statusstringYespending, approved, or rejected.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the asset was updated.
DELETE/api/articles/:id/assets

Delete article assets

api:write
Query Parameters
NameTypeRequiredDescription
assetIdstringNoSpecific asset ID to delete.
typestringNoDelete all assets of this type.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the assets were deleted.
POST/api/articles/bulk-write

Bulk generate articles

api:writeai:execute

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.

Body Parameters
NameTypeRequiredDescription
idsstring[]YesArticle IDs to process.
modestringNooutline_ready_content (default) writes Outline Ready articles; draft_full_article researches, outlines, and writes Draft articles.
Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
GET/api/articles/bulk-write

List bulk write sessions

api:read

Returns the 20 most recent sessions with their items.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
[]BulkWriteSession[]NoThe 20 most recent sessions, newest first, each shaped like the session detail response below.
GET/api/articles/bulk-write/:sessionId

Get bulk write session

api:read

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.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoSession ID.
statusstringNoqueued or preparing while waiting to start, then running, completed, stopped, or failed.
modestringYesoutline_ready_content or draft_full_article.
product_idstringYesProduct the run belongs to.
total_articlesnumberNoArticles in the run.
completed_articlesnumberNoArticles finished so far.
failed_articlesnumberNoArticles that failed.
error_messagestringYesWhy the run failed.
started_atstringYesWhen the run began.
completed_atstringYesWhen the run finished.
itemsobject[]NoOne entry per article in the run.
items[].idstringNoItem ID.
items[].article_idstringNoThe article being written.
items[].target_keywordstringNoThat article’s keyword.
items[].statusstringNopending, researching, generating_outline, writing, completed, failed, or cancelled.
items[].stepstringYesThe step currently running.
items[].error_messagestringYesWhy this article failed.
Example Response

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 }
  ]
}
PATCH/api/articles/bulk-write/:sessionId

Stop a bulk write session

api:write

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.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the run was stopped.
Example Response

200 OK — JSON

{ "success": true }
POST/api/articles/bulk-publish-cms

Bulk publish to CMS

api:write

Returns an SSE stream with progress.

Body Parameters
NameTypeRequiredDescription
idsstring[]YesArticle IDs to publish.
Success Response

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.

GET/api/products

List products

api:read

Returns an array of the products this key can reach, newest first.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
[]Product[]NoEvery product this key can reach, newest first.
[].idstringNoProduct ID.
[].namestringNoProduct name.
[].descriptionstringYesWhat the product does.
[].statusstringYesWhether the product is set up and active.
[].core_problemstringYesThe problem the product solves.
[].positioning_statementstringYesOne-line positioning.
[].differentiatorsarrayYesWhat sets the product apart, as { title, description }.
[].competitor_differentiatorsarrayYesHow the product differs from named competitors.
[].customer_storiesarrayYesCase 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_atstringYesWhen the site was last checked for new or changed customer stories.
[].featuresarrayYesProduct features.
[].use_casesstringYesWhat people use the product for.
[].target_audiencearrayYesWho the product is for.
[].value_propositionsarrayYesThe value the product promises.
[].content_pillarsarrayYesThemes the content programme is built around.
[].security_compliancearrayYesCertifications and compliance claims.
[].pricing_infostringYesPricing summary.
[].pricing_detailsstringYesFull pricing detail.
[].pricing_urlstringYesPricing page URL.
[].sitemap_urlstringYesWebsite or sitemap URL.
[].site_structureobjectYesThe site sections found on the website.
[].cta_signup_urlstringYesWhere calls to action send readers.
[].cta_button_textstringYesCall-to-action button label.
[].cta_image_urlstringYesCall-to-action image.
[].edge_docobjectYesThe positioning document the writer works from.
[].writing_stylesobjectYesThe saved writing style.
[].linkedin_writing_rulesstringYesExtra rules for LinkedIn posts.
[].publishing_cadence_per_weeknumberYesArticles planned per week.
[].publishing_start_day_of_weeknumberYesDay of week the schedule starts, 0 for Sunday.
[].refresh_publish_modestringYesHow refreshed articles are republished.
[].topic_auto_topupbooleanYesWhether the topic backlog tops itself up.
[].topic_auto_topup_min_backlognumberYesBacklog size that triggers a top-up.
[].search_location_codenumberYesDefault location code for keyword research.
[].search_language_codestringYesDefault language code for keyword research.
[].cover_style_promptstringYesPrompt describing the cover image style.
[].cover_style_profileobjectYesThe saved cover image style.
[].cover_style_profile_updated_atstringYesWhen the cover style last changed.
[].cover_style_selection_modestringYesHow the cover style is chosen.
[].cover_style_suggestionsarrayYesSuggested cover styles.
[].cover_style_updated_atstringYesWhen the cover style prompt last changed.
[].created_atstringNoISO 8601 creation time.
[].updated_atstringNoISO 8601 time of the last change.
POST/api/products

Create a product

api:write
Body Parameters
NameTypeRequiredDescription
namestringYesProduct name.
descriptionstringNoProduct description.
sitemap_urlstringNoWebsite or sitemap URL.
writing_styleobjectNoInitial writing style.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoProduct ID.
namestringNoProduct name.
descriptionstringYesWhat the product does.
statusstringYesWhether the product is set up and active.
core_problemstringYesThe problem the product solves.
positioning_statementstringYesOne-line positioning.
differentiatorsarrayYesWhat sets the product apart, as { title, description }.
competitor_differentiatorsarrayYesHow the product differs from named competitors.
customer_storiesarrayYesCase 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_atstringYesWhen the site was last checked for new or changed customer stories.
featuresarrayYesProduct features.
use_casesstringYesWhat people use the product for.
target_audiencearrayYesWho the product is for.
value_propositionsarrayYesThe value the product promises.
content_pillarsarrayYesThemes the content programme is built around.
security_compliancearrayYesCertifications and compliance claims.
pricing_infostringYesPricing summary.
pricing_detailsstringYesFull pricing detail.
pricing_urlstringYesPricing page URL.
sitemap_urlstringYesWebsite or sitemap URL.
site_structureobjectYesThe site sections found on the website.
cta_signup_urlstringYesWhere calls to action send readers.
cta_button_textstringYesCall-to-action button label.
cta_image_urlstringYesCall-to-action image.
edge_docobjectYesThe positioning document the writer works from.
writing_stylesobjectYesThe saved writing style.
linkedin_writing_rulesstringYesExtra rules for LinkedIn posts.
publishing_cadence_per_weeknumberYesArticles planned per week.
publishing_start_day_of_weeknumberYesDay of week the schedule starts, 0 for Sunday.
refresh_publish_modestringYesHow refreshed articles are republished.
topic_auto_topupbooleanYesWhether the topic backlog tops itself up.
topic_auto_topup_min_backlognumberYesBacklog size that triggers a top-up.
search_location_codenumberYesDefault location code for keyword research.
search_language_codestringYesDefault language code for keyword research.
cover_style_promptstringYesPrompt describing the cover image style.
cover_style_profileobjectYesThe saved cover image style.
cover_style_profile_updated_atstringYesWhen the cover style last changed.
cover_style_selection_modestringYesHow the cover style is chosen.
cover_style_suggestionsarrayYesSuggested cover styles.
cover_style_updated_atstringYesWhen the cover style prompt last changed.
created_atstringNoISO 8601 creation time.
updated_atstringNoISO 8601 time of the last change.
GET/api/products/:id

Get a product

api:read

Returns the product with its writing style.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoProduct ID.
namestringNoProduct name.
descriptionstringYesWhat the product does.
statusstringYesWhether the product is set up and active.
core_problemstringYesThe problem the product solves.
positioning_statementstringYesOne-line positioning.
differentiatorsarrayYesWhat sets the product apart, as { title, description }.
competitor_differentiatorsarrayYesHow the product differs from named competitors.
customer_storiesarrayYesCase 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_atstringYesWhen the site was last checked for new or changed customer stories.
featuresarrayYesProduct features.
use_casesstringYesWhat people use the product for.
target_audiencearrayYesWho the product is for.
value_propositionsarrayYesThe value the product promises.
content_pillarsarrayYesThemes the content programme is built around.
security_compliancearrayYesCertifications and compliance claims.
pricing_infostringYesPricing summary.
pricing_detailsstringYesFull pricing detail.
pricing_urlstringYesPricing page URL.
sitemap_urlstringYesWebsite or sitemap URL.
site_structureobjectYesThe site sections found on the website.
cta_signup_urlstringYesWhere calls to action send readers.
cta_button_textstringYesCall-to-action button label.
cta_image_urlstringYesCall-to-action image.
edge_docobjectYesThe positioning document the writer works from.
writing_stylesobjectYesThe saved writing style.
linkedin_writing_rulesstringYesExtra rules for LinkedIn posts.
publishing_cadence_per_weeknumberYesArticles planned per week.
publishing_start_day_of_weeknumberYesDay of week the schedule starts, 0 for Sunday.
refresh_publish_modestringYesHow refreshed articles are republished.
topic_auto_topupbooleanYesWhether the topic backlog tops itself up.
topic_auto_topup_min_backlognumberYesBacklog size that triggers a top-up.
search_location_codenumberYesDefault location code for keyword research.
search_language_codestringYesDefault language code for keyword research.
cover_style_promptstringYesPrompt describing the cover image style.
cover_style_profileobjectYesThe saved cover image style.
cover_style_profile_updated_atstringYesWhen the cover style last changed.
cover_style_selection_modestringYesHow the cover style is chosen.
cover_style_suggestionsarrayYesSuggested cover styles.
cover_style_updated_atstringYesWhen the cover style prompt last changed.
created_atstringNoISO 8601 creation time.
updated_atstringNoISO 8601 time of the last change.
PATCH/api/products/:id

Update a product

api:write

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.

Body Parameters
NameTypeRequiredDescription
writing_styleobjectNoReplaces the saved writing style.
expected_revisionnumberNoRevision the writing style edit is based on. Accepted at the top level or inside writing_style.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoProduct ID.
namestringNoProduct name.
descriptionstringYesWhat the product does.
statusstringYesWhether the product is set up and active.
core_problemstringYesThe problem the product solves.
positioning_statementstringYesOne-line positioning.
differentiatorsarrayYesWhat sets the product apart, as { title, description }.
competitor_differentiatorsarrayYesHow the product differs from named competitors.
customer_storiesarrayYesCase 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_atstringYesWhen the site was last checked for new or changed customer stories.
featuresarrayYesProduct features.
use_casesstringYesWhat people use the product for.
target_audiencearrayYesWho the product is for.
value_propositionsarrayYesThe value the product promises.
content_pillarsarrayYesThemes the content programme is built around.
security_compliancearrayYesCertifications and compliance claims.
pricing_infostringYesPricing summary.
pricing_detailsstringYesFull pricing detail.
pricing_urlstringYesPricing page URL.
sitemap_urlstringYesWebsite or sitemap URL.
site_structureobjectYesThe site sections found on the website.
cta_signup_urlstringYesWhere calls to action send readers.
cta_button_textstringYesCall-to-action button label.
cta_image_urlstringYesCall-to-action image.
edge_docobjectYesThe positioning document the writer works from.
writing_stylesobjectYesThe saved writing style.
linkedin_writing_rulesstringYesExtra rules for LinkedIn posts.
publishing_cadence_per_weeknumberYesArticles planned per week.
publishing_start_day_of_weeknumberYesDay of week the schedule starts, 0 for Sunday.
refresh_publish_modestringYesHow refreshed articles are republished.
topic_auto_topupbooleanYesWhether the topic backlog tops itself up.
topic_auto_topup_min_backlognumberYesBacklog size that triggers a top-up.
search_location_codenumberYesDefault location code for keyword research.
search_language_codestringYesDefault language code for keyword research.
cover_style_promptstringYesPrompt describing the cover image style.
cover_style_profileobjectYesThe saved cover image style.
cover_style_profile_updated_atstringYesWhen the cover style last changed.
cover_style_selection_modestringYesHow the cover style is chosen.
cover_style_suggestionsarrayYesSuggested cover styles.
cover_style_updated_atstringYesWhen the cover style prompt last changed.
created_atstringNoISO 8601 creation time.
updated_atstringNoISO 8601 time of the last change.
DELETE/api/products/:id

Delete a product

api:write
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the product was deleted.
Example Response

200 OK — JSON

{ "success": true }
POST/api/products/analyze-url

Analyze product URL

api:writeai:execute

Extract product details from a website.

Body Parameters
NameTypeRequiredDescription
urlstringYesWebsite URL.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
namestringYesProduct name read off the site.
descriptionstringYesWhat the product does.
core_problemstringYesThe problem it solves.
positioning_statementstringYesOne-line positioning.
differentiatorsarrayNoWhat sets it apart.
competitor_differentiatorsarrayNoHow it differs from named competitors.
featuresarrayNoFeatures found on the site.
target_audiencearrayNoWho it is for.
value_propositionsarrayNoThe value it promises.
use_casesstringYesWhat people use it for.
security_compliancearrayNoCertifications and compliance claims.
pricing_infostringYesPricing summary.
pricing_detailsstringYesFull pricing detail.
pricing_urlstringYesPricing page URL.
cta_signup_urlstringYesSign-up URL found on the site.
cta_button_textstringYesCall-to-action label.
sitemap_urlstringYesSitemap discovered for the site.
POST/api/products/analyze-writing-style

Extract writing style

api:writeai:execute
Body Parameters
NameTypeRequiredDescription
urlsstring[]YesBlog URLs to analyze. Max 5.
productNamestringNoProduct name for context.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
content_style_rulesstringNoThe writing style read off the sample posts, merged with the baseline.
analyzed_countnumberNoHow many posts were read.
failed_urlsstring[]NoURLs that could not be fetched.
POST/api/products/suggest-positioning

Suggest positioning

api:writeai:execute
Body Parameters
NameTypeRequiredDescription
productobjectYesProduct details.
answersobjectNoOptional questionnaire answers.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
positioning_statementstringYesSuggested one-line positioning.
differentiatorsarrayNoSuggested differentiators.
competitor_differentiatorsarrayNoSuggested competitor contrasts.
POST/api/products/extract-document

Extract product data from document

api:writeai:execute

Send as multipart/form-data.

Body Parameters
NameTypeRequiredDescription
fileFileYesPDF, TXT, or Markdown file.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
featuresarrayNoFeatures found in the document.
differentiatorsarrayNoDifferentiators found in the document.
pricing_detailsstringYesPricing detail found in the document.
positioning_statementstringYesPositioning the document implies.
competitor_differentiatorsarrayNoHow the product differs from each competitor the document names.
customer_storiesarrayNoCase 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.

POST/api/keywords/research

Run keyword research

api:writeai:execute
Body Parameters
NameTypeRequiredDescription
keywordsstring[]NoOne to five seed keywords. Use this or seed_keyword.
seed_keywordstringNoLegacy single seed keyword. Use this or keywords.
product_idstringNoProduct ID used to associate saved research and dedupe.
language_codestringNoLanguage code. Defaults to en.
location_codenumberNoNumeric location code. Defaults to United States (2840).
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
query_idstringYesThe saved query, or null when nothing usable was found. Read it back with GET /api/keywords/research/:queryId.
seed_keywordstringNoThe seed the research ran from.
total_resultsnumberNoHow many keywords were kept.
no_resultsbooleanNoTrue when the research found nothing usable.
ai_addednumberNoKeywords added on top of the provider results.
validated_ai_addednumberNoHow many of those survived validation.
resultsobject[]NoThe keywords found, highest search volume first.
results[].idstringNoResult ID, used to add the keyword to an import batch.
results[].keywordstringNoThe keyword.
results[].search_volumenumberYesMonthly searches.
results[].keyword_difficultynumberYesHow hard the keyword is to rank for.
results[].cpcnumberYesCost per click.
results[].competitionnumberYesPaid competition, 0 to 1.
results[].search_intentstringYesinformational, navigational, commercial, or transactional.
results[].serp_dataobjectYesCached results page detail, when one has been fetched.
GET/api/keywords/research/history

List keyword research history

api:read

Returns the 50 most recent research queries, newest first.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
queriesobject[]NoThe 50 most recent research queries, newest first.
queries[].idstringNoQuery ID.
queries[].seed_keywordstringNoThe seed the research started from.
queries[].language_codestringYesLanguage the research ran in.
queries[].location_codenumberYesLocation the research ran in.
queries[].total_resultsnumberYesHow many keywords the query found.
queries[].created_atstringNoWhen the research ran.
queries[].productobjectYesThe product, as { id, name }.
Example Response

200 OK — JSON

{
  "queries": [
    { "id": "query-id", "seed_keyword": "best crm", "total_results": 412, "created_at": "2026-09-01T10:00:00.000Z" }
  ]
}
GET/api/keywords/research/:queryId

Get keyword research results

api:read

Returns the saved query and its keyword rows, highest search volume first.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
queryobjectNoThe saved query, as listed in the research history.
resultsobject[]NoIts keyword rows, highest search volume first.
Example Response

200 OK — JSON

{
  "query": { "id": "query-id", "seed_keyword": "best crm" },
  "results": [{ "id": "result-id", "keyword": "best crm for startups", "search_volume": 1900 }]
}
POST/api/keywords/research/add-to-import

Add keyword results to import

api:write
Body Parameters
NameTypeRequiredDescription
result_idsstring[]YesKeyword result IDs.
product_idstringYesProduct ID.
batch_idstringNoExisting batch to add to. A new batch is created when omitted.
batch_namestringNoName for the new batch.
research_query_idstringNoResearch query the results came from.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
batch_idstringNoThe batch the keywords were added to, new or existing.
batch_namestringYesThat batch's name.
keywords_addednumberNoHow many keywords were added.
POST/api/keywords/research/serp

Fetch the live Google results page (results in order; competitors = organic domains)

api:write
Body Parameters
NameTypeRequiredDescription
keywordstringYesKeyword to check.
result_idstringNoResearch result to cache the SERP snapshot on. Cached snapshots are returned free for 30 days.
language_codestringNoDefaults to the research query language, else en.
location_codenumberNoDefaults to the research query location, else 2840 (US).
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
resultsobject[]NoThe organic results in the order Google returned them.
competitorsstring[]NoThe organic domains, in rank order.
serp_featuresobjectYesWhich result features the page carries.
cachedbooleanYesTrue when a snapshot from the last 30 days was reused at no cost.

Bulk Import

Create keyword batches and generate articles in bulk.

GET/api/bulk-import

List keyword import batches

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID filter.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
[]KeywordImportBatch[]NoBatches for the product, newest first.
[].idstringNoBatch ID.
[].namestringYesBatch name.
[].product_idstringNoProduct the batch belongs to.
[].plan_typestringNoseo_keywords or ai_visibility_answers.
[].research_query_idstringYesResearch query the batch came from.
[].created_atstringNoWhen the batch was created.
[].updated_atstringNoWhen the batch last changed.
[].keyword_importsobject[]NoThe keyword rows in the batch.
POST/api/bulk-import

Create keyword import batch

api:write
Body Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
keywordsobject[]YesKeyword rows.
namestringNoBatch name.
plan_typestringNoseo_keywords (default) or ai_visibility_answers.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
KeywordImportBatchNoThe new batch, returned with HTTP 201, shaped as in the batch detail response.
keyword_importsobject[]NoThe keyword rows that were created with it.
GET/api/bulk-import/batch/:batchId

Get import batch

api:read

Returns the batch with its keyword_imports rows.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoBatch ID.
namestringYesBatch name.
product_idstringNoProduct the batch belongs to.
plan_typestringNoseo_keywords or ai_visibility_answers.
keyword_importsobject[]NoThe keyword rows in the batch.
keyword_imports[].idstringNoRow ID, used to update or generate from it.
keyword_imports[].keywordstringNoThe keyword.
keyword_imports[].statusstringNoRow status.
keyword_imports[].anglestringYesAngle the article should take.
keyword_imports[].word_rangenumberYesTarget word count for the article.
keyword_imports[].custom_versionstringYesCustom instructions for the article.
PATCH/api/bulk-import/batch/:batchId

Rename an import batch

api:write
Body Parameters
NameTypeRequiredDescription
namestringYesNew batch name.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoBatch ID.
namestringNoThe new batch name.
DELETE/api/bulk-import/batch/:batchId

Delete an import batch

api:write

Deletes the batch and the keyword rows in it.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the batch and its rows were deleted.
Example Response

200 OK — JSON

{ "success": true }
PATCH/api/bulk-import/:id

Update a keyword import row

api:write

Returns 400 when the body contains none of these fields.

Body Parameters
NameTypeRequiredDescription
custom_versionstringNoCustom instructions for this keyword.
anglestringNoArticle angle for this keyword.
word_rangenumberNoTarget word count for this keyword.
statusstringNoRow status.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoRow ID.
keywordstringNoThe keyword.
statusstringNoRow status after the update.
anglestringYesAngle after the update.
word_rangenumberYesTarget word count after the update.
custom_versionstringYesCustom instructions after the update.
DELETE/api/bulk-import/:id

Delete a keyword import row

api:write
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the row was deleted.
Example Response

200 OK — JSON

{ "success": true }
POST/api/bulk-import/generate

Generate from batch

api:writeai:execute

Returns an SSE stream with progress.

Body Parameters
NameTypeRequiredDescription
idsstring[]YesKeyword import IDs to process.
Success Response

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.

GET/api/content-import

List content import batches

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID filter.
pagenumberNoPage number, starting at 1.
page_sizenumberNoItems per page. Default 50, max 100.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
batchesobject[]NoOne page of import batches, newest first.
batches[].idstringNoBatch ID.
batches[].namestringYesBatch name.
batches[].product_idstringNoProduct the batch belongs to.
batches[].source_typestringNositemap or the CMS provider it came from.
batches[].source_urlstringYesThe sitemap or site the batch read.
batches[].created_atstringNoWhen the batch was created.
batches[].content_importsobject[]NoThe pages found in the batch.
totalnumberNoTotal batches across all pages.
pagenumberNoThe page returned.
page_sizenumberNoRows per page.
has_morebooleanNoWhether a further page exists.
Example Response

200 OK — JSON

{
  "batches": [{ "id": "batch-id", "source_type": "sitemap", "status": "completed" }],
  "total": 12,
  "page": 1,
  "page_size": 50,
  "has_more": false
}
POST/api/content-import/batch

Create content import batch

api:write
Body Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
source_typestringYessitemap or CMS provider.
sitemap_urlstringNoSitemap URL for sitemap imports.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
ContentImportBatchNoThe new batch, returned with HTTP 201, shaped as in the batch detail response.
GET/api/content-import/batch/:batchId

Get content import batch

api:read
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoBatch ID.
namestringYesBatch name.
source_typestringNositemap or the CMS provider it came from.
statusstringYesWhere the batch has got to.
content_importsobject[]NoThe pages in the batch, each with its URL, title and import state.
DELETE/api/content-import/batch/:batchId

Delete content import batch

api:write
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the batch was deleted.
Example Response

200 OK — JSON

{ "success": true }
GET/api/content-import/batch/:batchId/events

Stream content import progress

api:read

SSE stream.

Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
POST/api/content-import/fetch

Fetch source content

api:write

Reads the sitemap or connected CMS and records what it finds against the batch.

Returns an SSE stream.

Body Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
source_typestringYessitemap or a connected CMS provider.
batch_idstringNoBatch to fetch into. A new one is created when omitted.
sitemap_urlstringNoSitemap URL for sitemap imports.
Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
POST/api/content-import/import

Import fetched content as articles

api:write

Returns an SSE stream.

Body Parameters
NameTypeRequiredDescription
idsstring[]YesFetched item IDs to import.
Success Response

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.

GET/api/ai-visibility/overview

Get AI Visibility overview

api:read

Returns aggregate metrics, trend points, provider breakdowns, top prompts to fix, alerts, and suggestions.

Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID. Defaults to the active product.
rangestringNo7d (default), 14d, 30d, 90d, or custom.
windowstringNoAlias for range, kept for older clients.
date_fromstringNoStart date (YYYY-MM-DD). Used when range is custom.
date_tostringNoEnd date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day.
providerstringNoNarrow to one answer engine: chatgpt, gemini, perplexity, claude, or google_aio. All engines when omitted.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
productProductNoThe product the numbers are for.
setup_requiredbooleanNoTrue until AI Visibility has been configured for the product.
metricsobjectNoHeadline numbers for the window: { visibilityScore, citationRate, shareOfVoice, allDetectedShareOfVoice, sentimentScore, prominenceIndex, sampleCount, promptObservationCount, confidence }.
previous_metricsobjectNoThe same numbers for the preceding window, for comparison.
metrics_truncatedbooleanNoTrue when the window held more samples than were read.
by_providerobjectNoThe same metrics keyed by engine: chatgpt, gemini, perplexity, claude, google_aio.
competitorsobject[]NoCompetitor brands with their share of voice, visibility and citation rate.
trend_competitorsobject[]NoThe competitors plotted in the trend series.
share_of_voice_trendobject[]NoShare of voice per day, as { date, share_of_voice, sample_count }.
share_of_voice_trend_granularitystringNoWhether the trend is bucketed by day or by week.
metric_trendobject[]NoEvery headline metric per day.
metric_trend_granularitystringNoWhether the metric trend is bucketed by day or by week.
trendobject[]NoMention split per day, as { date, own, competitor, other, competitors }.
top_prompts_to_fixobject[]NoPrompts where competitors lead, worst first.
promptsobject[]NoThe tracked prompts, as { id, prompt_text, intent_bucket, priority, active }.
alertsobject[]NoOpen alerts for the product.
insightsobject[]NoWhat changed in the window and why.
suggestionsobject[]NoRecommended next actions.
observation_daysnumberNoDays in the window that actually held samples.
window_daysnumberNoLength of the window in days.
date_rangeobjectNoThe window the numbers cover, as { key, from, to, days, label }.
providerstringYesThe engine the response was narrowed to, or null for all engines.
billing_planstringNoThe plan the account is on.
included_providersstring[]NoEngines the plan covers.
locked_providersstring[]NoEngines the plan does not cover.
provider_accessobject[]NoPer-engine availability and what unlocks it.
formulasobjectNoHow each headline metric is calculated.
GET/api/ai-visibility/settings

Get AI Visibility settings

api:read

Read the configured brand, competitors, aliases, providers, cadence, and setup state.

Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID. Defaults to the active product.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
productProductNoThe product the settings belong to.
settingsobjectNoThe configuration: { brand_name, brand_domain, brand_aliases, competitors, country, locale, persona, cadence, is_enabled, selected_providers }.
suggested_brand_domainstringYesDomain suggested from the product website when none is set.
active_prompts_countnumberNoHow many prompts are currently tracked.
active_prompt_limitnumberNoHow many the plan allows.
generated_prompt_targetnumberNoHow many prompts setup aims to produce.
billing_planstringNoThe plan the account is on.
included_providersstring[]NoEngines the plan covers.
locked_providersstring[]NoEngines the plan does not cover.
provider_accessobject[]NoPer-engine availability and what unlocks it.
selected_providersstring[]NoEngines chosen in settings.
effective_selected_providersstring[]NoEngines that will actually run, once the plan is applied.
can_manage_claude_addonbooleanNoWhether the account can change the Claude add-on.
GET/api/ai-visibility/prompts

List tracked prompts

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
pagenumberNoPage number. Default 1.
page_sizenumberNoPage size. Max 100.
activebooleanNoSet false to list inactive prompts.
include_metricsbooleanNoSet false to skip per-prompt metrics and return the list faster.
include_clustersbooleanNoSet true to include the cluster each prompt belongs to.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
promptsobject[]NoOne page of tracked prompts.
prompts[].idstringNoPrompt ID.
prompts[].prompt_textstringNoThe question put to the engines.
prompts[].intent_bucketstringYesThe intent the prompt represents.
prompts[].prioritynumberYesHow important the prompt is.
prompts[].activebooleanNoWhether the prompt is still being run.
prompts[].positionnumberYesDisplay order.
prompts[].sourcestringYesHow the prompt was created.
prompts[].last_run_atstringYesWhen the prompt last ran.
prompts[].metricsobjectYesPer-prompt metrics, unless include_metrics=false.
prompts[].top_competitorsobject[]YesThe brands that win this prompt.
competitor_suggestionsobject[]NoBrands seen in answers that are not yet tracked as competitors.
metrics_truncatedbooleanNoTrue when more samples existed than were read.
totalnumberNoTotal rows across all pages.
pagenumberNoThe page returned.
page_sizenumberNoRows per page.
GET/api/ai-visibility/prompts/:id

Get prompt detail

api:read

Returns prompt metrics, provider responses, citations, fan-out queries, competitor rows, and timeline data.

Query Parameters
NameTypeRequiredDescription
pagenumberNoResponse page number.
page_sizenumberNoResponse page size. Max 50.
citation_pagenumberNoCitation page number.
citation_page_sizenumberNoCitation page size. Max 50.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
promptobjectNoThe prompt, as listed.
metricsobjectNoMetrics for this prompt: { visibilityScore, citationRate, shareOfVoice, allDetectedShareOfVoice, sentimentScore, prominenceIndex, sampleCount, promptObservationCount, confidence }.
responsesobjectNoA page of engine answers, as { data, total, page, page_size }.
citationsobjectNoA page of the URLs those answers cited.
fanoutsobject[]YesThe follow-up queries engines ran.
competitorsobject[]NoBrands mentioned alongside yours in this prompt.
timelineobject[]NoHow this prompt has moved over time.
GET/api/ai-visibility/citations

List AI Visibility citations

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
providerstringNoNarrow to one answer engine: chatgpt, gemini, perplexity, claude, or google_aio. All engines when omitted.
categorystringNoCitation category filter.
prompt_idstringNoPrompt filter.
searchstringNoCase-insensitive title, domain, or URL search. Max 200 characters.
pagenumberNoPage number. Default 1.
page_sizenumberNoPage size. Max 100.
rangestringNo7d (default), 14d, 30d, 90d, or custom.
windowstringNoAlias for range, kept for older clients.
date_fromstringNoStart date (YYYY-MM-DD). Used when range is custom.
date_tostringNoEnd date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day.
viewstringNoall or own.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
citationsobject[]NoOne page of cited URLs.
citations[].idstringNoCitation ID.
citations[].urlstringNoThe URL that was cited.
citations[].normalized_urlstringNoThe URL with tracking and casing removed.
citations[].domainstringNoHost of the cited URL.
citations[].titlestringYesPage title.
citations[].descriptionstringYesPage description.
citations[].providerstringNoThe engine that cited it.
citations[].is_own_domainbooleanNoWhether the URL is on your own site.
citations[].positionnumberYesWhere it appeared in the answer.
citations[].source_typestringYesWhat kind of source it is.
citations[].first_seen_atstringNoWhen it was first cited.
citations[].prompt_idstringNoThe prompt whose answer cited it.
websitesobject[]NoThe cited URLs rolled up per page, with content category and citation count.
category_summariesobject[]NoCounts per content category.
domainsobject[]NoCounts per domain, as { domain, citation_count, website_count, own }.
total_citationsnumberNoCitations matching the filters.
total_all_citationsnumberNoCitations in the window before filters.
total_websitesnumberNoDistinct pages matching the filters.
total_all_websitesnumberNoDistinct pages in the window before filters.
truncatedbooleanNoTrue when more citations existed than were read.
date_rangeobjectNoThe window the numbers cover, as { key, from, to, days, label }.
totalnumberNoTotal rows across all pages.
pagenumberNoThe page returned.
page_sizenumberNoRows per page.
GET/api/ai-visibility/competitors

List competitor profiles

api:read

Returns lightweight competitor profile summaries with AI visibility, share of voice, citation rate, citation URL count, and stable identifiers.

Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
competitorstringNoStable competitor identifier or legacy name used to scope gap_prompts.
rangestringNo7d (default), 14d, 30d, 90d, or custom.
windowstringNoAlias for range, kept for older clients.
date_fromstringNoStart date (YYYY-MM-DD). Used when range is custom.
date_tostringNoEnd date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
competitorsobject[]NoThe competitors you track, with share of voice, visibility, citation rate, prominence and sentiment.
field_brandsobject[]NoEvery brand seen in answers, tracked or not, with the same numbers.
competitor_profilesobject[]NoProfile 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.
ownobjectNoYour own numbers, for comparison: { visibilityScore, citationRate, shareOfVoice, allDetectedShareOfVoice, sentimentScore, prominenceIndex, sampleCount, promptObservationCount, confidence }.
gap_promptsobject[]NoPrompts where a competitor leads you, widest gap first.
selected_competitorstringYesThe competitor gap_prompts was scoped to.
metrics_truncatedbooleanNoTrue when more samples existed than were read.
totalnumberNoHow many competitors were returned.
window_daysnumberNoLength of the window in days.
date_rangeobjectNoThe window the numbers cover, as { key, from, to, days, label }.
GET/api/ai-visibility/competitors/:competitor

Get competitor profile

api:read

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.

Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
pagenumberNoCitation URL page number. Default 1. Clamped to the last page of the filtered set.
page_sizenumberNoCitation URL page size. Max 100.
citation_categorystringNoFilter citation URLs by content category (listicle, marketing_page, comparison, …).
citation_providerstringNoFilter citation URLs by AI engine.
citation_promptstringNoFilter citation URLs to those cited under one prompt id.
citation_searchstringNoFree-text match against citation URL and title. Max 200 chars.
rangestringNo7d (default), 14d, 30d, 90d, or custom.
windowstringNoAlias for range, kept for older clients.
date_fromstringNoStart date (YYYY-MM-DD). Used when range is custom.
date_tostringNoEnd date (YYYY-MM-DD). Used when range is custom. Clamped to the last complete day.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
competitorobjectNoThe competitor profile.
citation_urlsobject[]NoA page of URLs cited for this competitor, each with its content category and a per-prompt breakdown.
totalnumberNoCitation URLs matching the filters.
pagenumberNoThe page returned, clamped to the last page of the filtered set.
page_sizenumberNoRows per page.
date_rangeobjectNoThe window the numbers cover, as { key, from, to, days, label }.
GET/api/ai-visibility/alerts

List AI Visibility alerts

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
statusstringNoAlert status filter.
pagenumberNoPage number.
page_sizenumberNoPage size. Max 100.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
alertsobject[]NoOne page of alerts, newest first.
alerts[].idstringNoAlert ID.
alerts[].alert_typestringNoWhat kind of change was detected.
alerts[].severitystringNoHow serious it is.
alerts[].titlestringNoOne-line summary.
alerts[].messagestringNoThe detail behind the alert.
alerts[].statusstringNoWhether the alert is still open.
alerts[].metric_keystringYesThe metric that moved.
alerts[].metric_deltanumberYesHow far it moved.
alerts[].providerstringYesThe engine involved.
alerts[].prompt_idstringYesThe prompt involved.
alerts[].detected_atstringNoWhen it was detected.
totalnumberNoTotal rows across all pages.
pagenumberNoThe page returned.
page_sizenumberNoRows per page.
GET/api/ai-visibility/suggestions

List AI Visibility suggestions

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
statusstringNoSuggestion status filter.
pagenumberNoPage number.
page_sizenumberNoPage size. Max 100.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
productobjectNoThe product, as { id, name }.
suggestionsobject[]NoOne page of recommended actions.
suggestions[].idstringNoSuggestion ID.
suggestions[].titlestringNoWhat to do.
suggestions[].bodystringNoWhy it is worth doing.
suggestions[].action_typestringNoThe kind of action it asks for.
suggestions[].categorystringYesWhat area it covers.
suggestions[].urgencystringYesHow soon it matters.
suggestions[].effortstringYesRough effort involved.
suggestions[].week_startstringYesThe week the suggestion belongs to.
totalnumberNoTotal rows across all pages.
pagenumberNoThe page returned.
page_sizenumberNoRows per page.
GET/api/ai-visibility/runs

List AI Visibility runs

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
pagenumberNoPage number.
page_sizenumberNoPage size. Max 100.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
runsobject[]NoOne page of runs, newest first.
runs[].idstringNoRun ID.
runs[].run_kindstringNoWhat triggered the run.
runs[].run_datestringNoThe day the run covers.
runs[].statusstringNoWhere the run got to.
runs[].countrystringYesCountry the prompts were asked from.
runs[].prompts_countnumberNoPrompts included in the run.
runs[].samples_expectednumberNoAnswers the run set out to collect.
runs[].samples_completednumberNoAnswers it actually collected.
runs[].provider_stateobjectYesPer-engine outcome for the run.
runs[].errorstringYesWhy the run failed.
runs[].started_atstringYesWhen the run began.
runs[].completed_atstringYesWhen it finished.
totalnumberNoTotal rows across all pages.
pagenumberNoThe page returned.
page_sizenumberNoRows per page.

SEO Audits

Create, inspect, and process technical SEO audits.

GET/api/seo-audit

List audits

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID filter.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
[]Audit[]NoAudits for the product, newest first.
[].idstringNoAudit ID.
[].product_idstringYesProduct the audit belongs to.
[].site_urlstringNoThe site that was audited.
[].site_labelstringYesDisplay name for the site.
[].statusstringNoWhere the audit has got to.
[].total_pagesnumberNoPages the audit set out to cover.
[].crawled_pagesnumberNoPages fetched so far.
[].analyzed_pagesnumberNoPages scored so far.
[].summaryobjectYesThe written summary, with overallScore, topIssues, quickWins and issue counts.
[].summary_statusstringYesWhether the summary is ready.
[].summary_historyarrayYesPrevious summaries, newest first.
[].optionsobjectYesHow the audit was configured: { mode, auditSource, skipLighthouse }.
[].link_graphobjectYesHow the site links to itself.
[].link_analysisobjectYesWhat the link analysis found.
[].link_analysis_statusstringYesWhether link analysis is ready.
[].linking_reportobjectYesInternal linking recommendations.
[].link_health_statusstringYesWhether the broken-link check is ready.
[].link_health_summaryobjectYesBroken and redirecting links found.
[].link_health_completed_atstringYesWhen the link check finished.
[].lighthouse_statusstringYesWhether Lighthouse has run.
[].lighthouse_attempted_pagesnumberYesPages Lighthouse tried.
[].lighthouse_completed_pagesnumberYesPages Lighthouse finished.
[].lighthouse_completed_atstringYesWhen Lighthouse finished.
[].created_atstringNoWhen the audit was created.
[].updated_atstringNoWhen it last changed.
POST/api/seo-audit

Create an audit

api:write

Creates an audit job and auto-starts background processing when possible.

Background job. Read GET /api/seo-audit/:id for status and page counts.

Body Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID.
sitemap_urlstringNoWebsite or sitemap URL.
urlsstring[]NoExplicit URLs to audit.
modestringNofull or lite.
skip_lighthousebooleanNoSkip Lighthouse analysis.
audit_sourcestringNoproduct to audit the product website, url to audit sitemap_url on its own. Defaults to url when no product is given.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
AuditNoThe new audit, shaped as in the list above, returned with HTTP 201. Poll GET /api/seo-audit/:id for progress.
product_namestringYesName of the product the audit belongs to.
page_countnumberNoHow many URLs the audit will cover.
discovery_sourcestringNoHow those URLs were found.
GET/api/seo-audit/:id

Get audit details

api:read
Query Parameters
NameTypeRequiredDescription
slimbooleanNoSet to 1 for lightweight polling.
no_pagesbooleanNoSet to 1 to return audit metadata without page rows.
include_report_databooleanNoSet to 1 to include the full report payload.
page_limitnumberNoNumber of page rows to return.
page_offsetnumberNoOffset into the page rows.
statusstringNoFilter page rows by crawl status. Default all.
scorestringNoFilter page rows by score band. Default all.
qstringNoSearch page rows by URL or title.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoAudit ID.
product_idstringYesProduct the audit belongs to.
site_urlstringNoThe site that was audited.
site_labelstringYesDisplay name for the site.
statusstringNoWhere the audit has got to.
total_pagesnumberNoPages the audit set out to cover.
crawled_pagesnumberNoPages fetched so far.
analyzed_pagesnumberNoPages scored so far.
summaryobjectYesThe written summary, with overallScore, topIssues, quickWins and issue counts.
summary_statusstringYesWhether the summary is ready.
summary_historyarrayYesPrevious summaries, newest first.
optionsobjectYesHow the audit was configured: { mode, auditSource, skipLighthouse }.
link_graphobjectYesHow the site links to itself.
link_analysisobjectYesWhat the link analysis found.
link_analysis_statusstringYesWhether link analysis is ready.
linking_reportobjectYesInternal linking recommendations.
link_health_statusstringYesWhether the broken-link check is ready.
link_health_summaryobjectYesBroken and redirecting links found.
link_health_completed_atstringYesWhen the link check finished.
lighthouse_statusstringYesWhether Lighthouse has run.
lighthouse_attempted_pagesnumberYesPages Lighthouse tried.
lighthouse_completed_pagesnumberYesPages Lighthouse finished.
lighthouse_completed_atstringYesWhen Lighthouse finished.
created_atstringNoWhen the audit was created.
updated_atstringNoWhen it last changed.
pagesobject[]NoThe crawled pages, with their scores and issues. Empty when no_pages=1.
page_countsobjectNoPage totals per crawl status.
page_titlesobjectYesPage titles keyed by page id.
crawlRuntimeobjectYesLive progress: { phase, currentUrl, processedUrls, analyzedUrls, totalUrls, failedUrls, discardedUrls, crawlTargetSeconds, crawlEtaSeconds, analysisEtaSeconds, summaryEtaSeconds, overallEtaSeconds }.
DELETE/api/seo-audit/:id

Delete an audit

api:write

Deletes the audit and its crawled pages.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
successbooleanNoTrue when the audit and its pages were deleted.
Example Response

200 OK — JSON

{ "success": true }
POST/api/seo-audit/:id/process

Start or continue audit processing

api:write

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.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
progressobjectNoCounts so far, as { total, crawled, analyzed }.
hasMorebooleanNoWhether work remains. Call again while true.
pagesobject[]NoPages finished by this call.
crawlRuntimeobjectYesLive progress and time estimates, as on the audit detail endpoint.
pausedbooleanYesReturned with HTTP 409 when the audit is paused.
Example Response

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
  }
}
POST/api/seo-audit/:id/summary

Get or queue the audit summary

api:writeai:execute

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.

Response Fields — 202 Accepted — JSON
FieldTypeNullableDescription
summaryobjectYesThe finished summary, on a completed audit.
linkGraphobjectYesThe link graph that goes with it.
queuedbooleanYesReturned with HTTP 202 when the audit is still running.
progressobjectYesCounts so far, alongside queued.
hasMorebooleanYesWhether work remains, alongside queued.
pausedbooleanYesReturned with HTTP 409 when the audit is paused.
Example Response

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." }
POST/api/seo-audit/:id/link-analysis

Run link analysis

api:writeai:execute

Returns an SSE stream with progress.

Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
GET/api/seo-audit/:id/insights

Get audit insights

api:read

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.

Response Fields — 200 OK — JSON
FieldTypeNullableDescription
insightsobjectNoThe findings: { diagnosis, stats, priorityActions, pageOpportunities, metadataQueue, performancePriorities }.
insights.diagnosisstringNoWhat is holding the site back, in a sentence.
insights.statsobjectNo{ analyzedPages, averageScore, criticalPages, quickWins }.
insights.priorityActionsobject[]NoWhat to fix first, each with title, problem, fix, impact, effort, scope and affected pages.
insights.pageOpportunitiesobject[]NoPages worth the most attention.
insights.metadataQueueobject[]NoTitles and descriptions worth rewriting.
insights.performancePrioritiesobject[]NoSpeed problems worth fixing.
score_distributionobjectNoHow page scores fall, as { good, fair, poor, total }.
copy_statusstringNopending or generating while the written explanations are being prepared, then ready.
Example Response

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"
}
GET/api/seo-audit/:id/page/:pageId

Get audited page details

api:read

Returns one crawled page with its checks and scores, plus a summary of the audit it belongs to.

Query Parameters
NameTypeRequiredDescription
viewstringNofull (default) returns everything. Narrow the payload with summary, metadata, lighthouse, deep-analysis, or prompt-response.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
idstringNoPage ID.
urlstringNoThe page URL.
statusstringNoWhere the page got to in the crawl.
scorenumberYesThe page score.
analysisobjectYesThe checks that were run and what they found.
page_summaryobjectYesA short summary of the page.
crawl_dataobjectYesMetadata and structure read from the page. The raw body is never included.
lighthouse_dataobjectYesLighthouse results, when Lighthouse ran.
deep_analysisobjectYesDeep analysis of the page, when one has been run.
errorstringYesWhy the page could not be processed.
auditobjectNoThe audit the page belongs to.
POST/api/seo-audit/:id/page/:pageId/deep-analysis

Run deep page analysis

api:writeai:execute

Returns an SSE stream with progress.

Success Response

200 OK — text/event-stream

event: progress
data: {"status":"running","processed":12,"total":50}

event: done
data: {"status":"completed","id":"..."}
POST/api/seo-audit/preview-sitemap

Preview sitemap URLs

api:write
Body Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
urlsstring[]NoThe URLs the audit would cover.
sitemap_urlstringYesThe sitemap the URLs came from.
totalnumberNoHow many URLs were found.
limitedbooleanNoTrue when the list was cut to the plan limit.
limitnumberNoThe most URLs your plan allows in one audit.

Search Console

Read cached Google Search Console performance data.

GET/api/gsc/connections

List Search Console connections

api:read

Returns the Search Console properties connected to a product.

Query Parameters
NameTypeRequiredDescription
product_idstringNoProduct ID. Defaults to the active product.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
[]Connection[]NoThe Search Console properties connected to the product.
[].idstringNoConnection ID.
[].product_idstringNoProduct the connection belongs to.
[].site_urlstringNoThe Search Console property, such as sc-domain:example.com.
[].permission_levelstringYesThe access Google grants on that property.
[].is_activebooleanNoWhether the connection is in use.
[].last_synced_atstringYesWhen data was last pulled.
[].created_atstringNoWhen the connection was made.
[].updated_atstringNoWhen it last changed.
Example Response

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"
  }
]
GET/api/gsc/performance

Get performance data

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
date_fromstringNoStart date.
date_tostringNoEnd date.
article_idstringNoArticle filter.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
total_clicksnumberNoClicks across the period.
total_impressionsnumberNoImpressions across the period.
avg_ctrnumberNoAverage click-through rate, 0 to 1.
avg_positionnumberNoAverage position across the period.
articlesobject[]NoPer-article performance.
articles[].page_urlstringNoThe published URL.
articles[].clicksnumberNoClicks for that page.
articles[].impressionsnumberNoImpressions for that page.
articles[].ctrnumberNoClick-through rate, 0 to 1.
articles[].avg_positionnumberNoAverage position for that page.
GET/api/gsc/top-queries

Get top search queries

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
date_fromstringNoStart date.
date_tostringNoEnd date.
limitnumberNoNumber of queries to return. Default 20.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
queriesobject[]NoThe best-performing search queries, most clicks first.
queries[].querystringNoWhat the row is for.
queries[].clicksnumberNoClicks in the period.
queries[].impressionsnumberNoImpressions in the period.
queries[].ctrnumberNoClick-through rate, 0 to 1.
queries[].positionnumberNoAverage position.
Example Response

200 OK — JSON

{
  "queries": [
    { "query": "best crm for startups", "clicks": 84, "impressions": 2100, "ctr": 0.04, "position": 7.2 }
  ]
}
GET/api/gsc/article-analytics

Get article analytics

api:read

Search performance for one published article, broken down by day, query, device, country, and search appearance.

Query Parameters
NameTypeRequiredDescription
article_idstringYesArticle ID.
date_fromstringNoStart date.
date_tostringNoEnd date.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
summaryobjectNoTotals for the article, as { clicks, impressions, ctr, position }.
dailyobject[]NoPerformance per day.
queriesobject[]NoThe queries the article ranks for.
devicesobject[]NoPerformance per device.
countriesobject[]NoPerformance per country.
searchAppearancesobject[]NoPerformance per search appearance type.
Example Response

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": []
}
GET/api/gsc/site-analytics

Get site analytics

api:read
Query Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
date_fromstringNoStart date.
date_tostringNoEnd date.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
dailyobject[]NoSite performance per day.
daily[].datestringNoWhat the row is for.
daily[].clicksnumberNoClicks in the period.
daily[].impressionsnumberNoImpressions in the period.
daily[].ctrnumberNoClick-through rate, 0 to 1.
daily[].positionnumberNoAverage position.
searchAppearancesobject[]NoPerformance per search appearance type.
searchAppearances[].searchAppearancestringNoWhat the row is for.
searchAppearances[].clicksnumberNoClicks in the period.
searchAppearances[].impressionsnumberNoImpressions in the period.
searchAppearances[].ctrnumberNoClick-through rate, 0 to 1.
searchAppearances[].positionnumberNoAverage position.
Example Response

200 OK — JSON

{
  "daily": [{ "date": "2026-09-01", "clicks": 140, "impressions": 5200, "ctr": 0.027, "position": 12.4 }],
  "searchAppearances": []
}
GET/api/gsc/pages

Get page-level performance

api:read

Returns 403 when the product has no Search Console connection.

Query Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
pagesobject[]NoPerformance per page.
pages[].urlstringNoThe page URL.
pages[].clicksnumberNoClicks in the period.
pages[].impressionsnumberNoImpressions in the period.
Example Response

200 OK — JSON

{
  "pages": [
    { "page": "https://example.com/blog/best-crm", "clicks": 84, "impressions": 2100, "ctr": 0.04, "position": 7.2 }
  ]
}
GET/api/gsc/agent-suggestions

Get Search Console suggestions

api:read

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.

Query Parameters
NameTypeRequiredDescription
product_idstringYesProduct ID.
forcebooleanNoSet true to ignore the cache and produce fresh suggestions.
refresh_empty_no_databooleanNoSet true to retry a cached result that came back empty.
Response Fields — 200 OK — JSON
FieldTypeNullableDescription
suggestionsobject[]NoRecommended next actions drawn from the Search Console data.
generated_atstringYesWhen the suggestions were produced.
cachedbooleanYesTrue when a previous result was reused rather than generating fresh ones.