Articles
List and read finished articles (markdown + HTML), and start article generation for any keyword.
Articles move through planned → writing → ready → published (plus failed
and archived). The API reads every state and can start generation.
On a site with additional languages, each language
version is its own article with its own id, status, and slug. They share a
translation_group_id, and each carries its own language.
List articles
GET /api/v1/sites/{site_id}/articles?status=ready&search=&language=&page=1&per_page=20Bodies are omitted from lists — fetch one article for content.
| Param | Notes |
|---|---|
status | planned · writing · ready · published · failed · archived |
search | Substring match on title or keyword. |
language | Only articles written in this code (e.g. de). See the note below. |
{
"data": [
{
"id": "a917…",
"status": "published",
"title": "The 7 Best Privacy-First Analytics Tools in 2026",
"language": "en",
"translation_group_id": null,
"keyword": "privacy first analytics",
"target_keyword": "privacy first analytics",
"word_count": 2140,
"seo_score": 92,
"scheduled_for": "2026-07-03T08:00:00.000Z",
"generated_at": "2026-07-03T08:09:31.000Z",
"published_at": "2026-07-03T08:10:02.000Z",
"gsc_index_state": "indexed",
"created_at": "2026-07-01T11:00:00.000Z"
}
],
"page": 1,
"per_page": 20,
"total": 58
}One row per language version
A multi-language site returns every version as its own row, so total counts versions rather than
topics. All versions of one article share a keyword — that's the keyword the original targets
— so language is what tells them apart, and target_keyword is the phrase that version actually
goes after. Filter with ?language=de for one language, or group by translation_group_id to
reassemble a topic. translation_group_id is null for an article with no translations.
Get an article
GET /api/v1/sites/{site_id}/articles/{article_id}The full piece: markdown and HTML bodies, per-check SEO scorecard, cited sources, image gallery, and publish state.
{
"id": "a917…",
"status": "ready",
"title": "…",
"slug": "best-privacy-first-analytics-tools",
"meta_description": "…",
"language": "en",
"translation_group_id": null,
"keyword": {
"text": "privacy first analytics",
"target": "privacy first analytics",
"volume": 480,
"difficulty": 31
},
"body_markdown": "## …",
"body_html": "<h2>…",
"word_count": 2140,
"seo_score": 92,
"seo_checks": [
{ "id": "title-length", "label": "Title length", "status": "pass", "detail": "…" }
],
"sources": [{ "url": "https://…", "title": "…" }],
"featured_image_url": "https://cdn.…/hero.webp",
"images": [{ "url": "https://cdn.…/inline-1.webp", "alt_text": "…" }],
"external_url": null,
"gsc_index_state": null,
"failure_reason": null,
"scheduled_for": "…",
"generated_at": "…",
"published_at": null,
"created_at": "…"
}Generate an article (asynchronous)
POST /api/v1/sites/{site_id}/articles/generateTarget an existing library keyword by id, or pass a raw keyword — it's matched to the library or created on the fly:
{ "keyword_id": "7b21…" }{ "keyword": "webhook vs polling for analytics events" }Returns 202:
{
"article_id": "c2d8…",
"status": "writing",
"queued": true,
"language": "en",
"additional_languages": ["de", "es"],
"credits_expected": 1,
"message": "Generation started. A full article takes several minutes — poll GET /v1/sites/{site_id}/articles/{article_id} until status is \"ready\". This site also publishes in de, es, so once the en article is ready it is localized into 2 more, each its own article sharing this one's translation_group_id. Those are covered by the language add-on and cost no extra article credits."
}Notes for agents:
- Generation consumes exactly one article credit, whatever the language count —
translations are covered by a paid add-on, not metered.
403 quota_exceededwhen spent,403 site_inactivewithout a live subscription. - One call can produce several articles.
article_idis the original; the translations don't exist yet, and are created once it'sready. To find them, pollGET /sites/{site_id}/articles?language={code}or match ontranslation_group_id— they aren't listed in this response because they haven't been created at that point. Checkadditional_languagesonGET /sites/{site_id}to know what's coming. - If the keyword already has an article in flight or finished, nothing is
double-generated — you get that article's id with
"queued": false. - Rate limit: 15/hour per account on top of the quota.
- Publishing follows the site's connected integrations and autopilot settings —
a
readyarticle auto-publishes exactly as if the dashboard produced it.