Public API · v1

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.

Authorization: Bearer YOUR_API_KEY

Base URL

The surface is path-versioned. Breaking changes ship under a new version prefix.

https://api.roboad.ai/api/public/v1

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"
}
Autopilot eligibility. 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

pendingqueuedrunning

The run is still working.

Terminal — stop

completedcompleted_with_errorsfailedcancelled

The run finished; read the item (or read error).

Needs human review — stop

awaiting_review

A 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 limit120 requests / 60 seconds, per organization
Over limit429 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.