API Documentation
Drive the full content lifecycle programmatically — discover a brand, pick or create a topic, generate, poll, and read the draft. Every call is scoped to the organization that owns your API key.
Quick Start
The RoboWrite Public API is a JSON REST API. Authenticate with an organization API key, then verify it works:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.roboad.ai/api/public/v1/ping"
Integrating with a coding agent?
We publish a complete, agent-ready version of these docs at robowrite.ai/llms-full.txt. Paste it into Claude, Cursor, or Copilot and it has everything needed to integrate — conventions, the full endpoint reference, request/response shapes, and a copy-paste end-to-end example.
Authentication
Every request requires your organization API key as a Bearer token. Create and manage keys in your RoboWrite dashboard under Settings → API keys. Each key is bound to a single organization; all reads and writes are scoped to it — there is no cross-tenant access. A missing or malformed header returns 401.
Base URL
The surface is path-versioned. Breaking changes ship under a new version prefix.
Clients & User-Agent
api.roboad.ai sits behind Cloudflare bot protection, so non-browser clients must send a normal, non-empty User-Agent header. Bare default agents such as Python's urllib are blocked at the edge: you get a 403 with an empty body (Cloudflare error 1010) before the request reaches the API — so there is no { detail } envelope to read. curl, httpx, and requests send an acceptable UA by default; if you build raw urllib requests, set one explicitly.
Conventions
Pagination
List endpoints return a uniform envelope: { items, total, limit, offset, has_more }. Control with ?limit= (1–200, default 50) and ?offset=; total is the full filtered count. A few endpoints return their own documented shape (e.g. keyword inventory, topic scores, brief sections).
Errors
Failures return the documented status code with a { "detail": "…" } body. Common codes: 401 (bad key), 404 (not found or not owned by your org), 409 (wrong state), 422 (invalid input — unknown body keys are rejected), 429 (rate limited), 503 (retry). The one exception with no JSON body is the Cloudflare 403 above.
Idempotency
Spend-sensitive write endpoints accept an Idempotency-Key header; a retry with the same key replays the original response (with Idempotency-Replayed: true) instead of repeating the operation. It is required on POST /content/items and optional on POST /brands/{brand_id}/topics, POST /content/generate, and POST /content/items/{item_id}/generate. Other writes (e.g. POST /briefs) do not dedupe — a dropped-response retry can create a duplicate. A same-key retry replays the original result, so changing the body under the same key does not start a new operation — use a fresh key when changing generation options.
Generate content from a topic
The fastest path to a generation. The body is exactly { "topic_id": "<uuid>" } — it does not accept direction or citation_mode (those live on the item path, POST /content/items/{item_id}/generate). Send an Idempotency-Key.
POST /content/generate
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Idempotency-Key: <opaque-unique-key>
{ "topic_id": "<uuid>" }The call returns 202 Accepted with a job to poll:
HTTP/1.1 202 Accepted
{
"job_id": "…",
"workflow_id": "…",
"status": "running"
}POST /content/generate only runs for autopilot-eligible topics; ineligible ones return 409. Eligible: auto_brief, light_review, and topics with no decision band yet (a topic you just created). Ineligible: expert_review, reject_or_hold. Filter on the eligible_for_autopilot flag from GET /topics?brand_id=… rather than firing blindly. A freshly created topic (POST /brands/{brand_id}/topics) has no band yet, so it is always eligible.Poll the job
Poll GET /jobs/{job_id} until the status stops polling. content_item_id and content_version_id arrive on the polled job (not on the 202), populated by the time the run reaches a terminal or review state.
In-flight — keep polling
pendingqueuedrunningThe run is still working.
Terminal — stop
completedcompleted_with_errorsfailedcancelledThe run finished; read the item (or read error).
Needs human review — stop
awaiting_reviewA draft was produced but routed to the dashboard review inbox (e.g. the brand has no connected CMS). The draft is ready — fetch it via GET /content/{content_item_id}.
GET /jobs/{job_id}
{
"job_id": "…",
"status": "awaiting_review",
"content_item_id": "…",
"content_version_id": "…",
"error": null
}Read the content
Fetch the item with all its versions. Add ?include=scoring for quality scores and ?include=sources for the research sources cited in each version (combine with a comma). Both are opt-in and additive — without include they are null; guard on presence, not on null vs [].
GET /content/{content_item_id}?include=scoring,sources
{
"id": "…",
"title": "…",
"status": "draft",
"versions": [
{
"version_number": 1,
"markdown_body": "# …",
"word_count": 1240,
"sources": [
{ "marker": 1, "source_name": "…", "source_url": "https://…" }
]
}
]
}Endpoint reference
The full v1 surface. Every collection endpoint is org-scoped, filterable where noted, and paginated.
Discovery
- GET
/pingLiveness check - GET
/brandsList brands you own - GET
/brands/{brand_id}/keywordsKeyword inventory + market data (custom shape) - GET · POST
/propertiesList or create publishing properties
Topics
- GET
/topicsList topics — ?brand_id= is required - POST
/brands/{brand_id}/topicsCreate a topic from your own input (eligible immediately) - GET
/topics/{topic_id}/scoresCached opportunity scores (bare array)
Briefs & content
- GET · POST
/briefsCreate or list briefs - GET · PATCH
/briefs/{brief_id}Get or edit a brief (edits are auto-versioned) - POST
/content/itemsCreate a content item (Idempotency-Key required) - GET
/content/{content_item_id}Get an item + versions (?include=scoring,sources) - PATCH · DELETE
/content/items/{item_id}Update or soft-delete a content item
Generation & jobs
- POST
/content/generateGenerate from a topic → 202 + job - POST
/content/items/{item_id}/generateGenerate / rewrite / vary (direction, citation_mode) - GET
/jobs/{job_id}Poll a generation job
Taxonomy (full CRUD)
- GET · POST · …
/pillars · /authors · /categories · /tagsManage the org's content taxonomy
Documents (read)
- GET
/documentsDocument library metadata + summaries - GET
/imported-contentCMS-imported / external content
Rate Limits
A single fixed window applies per organization — there are no per-plan request tiers. Exceeding it returns 429; back off and retry.
| Default limit | 120 requests / 60 seconds, per organization |
| Over limit | 429 Too Many Requests — retry after the window |
Not in v1 yet
These are deliberately out of v1 — design around them:
- Publishing to a connected CMS — publish from the dashboard for now. (This is why headless generation for a no-CMS brand lands in
awaiting_review.) - Completion webhooks — completion is poll-based today (
GET /jobs/{job_id}); push webhooks are planned. - Direct document file-download URLs — metadata and summaries are available; original-file signed URLs are not yet exposed.
- Pillar-strategy endpoints — a separate upcoming capability.
Changelog
2026-07 · Review-state polling
Jobs can now report awaiting_review — a stop-polling state for topic generations that produced a draft but were routed to the dashboard review inbox (e.g. no connected CMS). The poll response exposes content_item_id and content_version_id so the parked draft is fetchable via the API. Published this agent-ready guide at /llms-full.txt.
