Push and manage articles in Markdown with one API key. You never need to know Payload's Lexical JSON. Every write goes through Payload as you, so contributor / editor / admin rules still apply.

Machine-readable spec: OpenAPI 3.1. This page as Markdown: /developers.md.

Get an API key

Ask your editor for an account. An admin creates the user in this installation; user creation is restricted to admins:

  1. Email, name, password as usual.
  2. Role Contributor (the default). Contributors can create and edit their own drafts; they cannot publish.
  3. Tick Is agent so the audit trail marks the account as automated.
  4. Tick Enable API key and copy the key. Payload shows it once.

Editors and admins get the same header; they can publish.

Auth headers

Official Payload header:

Authorization: users API-Key <key>

Also accepted:

Authorization: Bearer <key>

Every /api/v1 route except /api/v1/openapi.json returns 401 when the caller is not a users account.

Editorial rules

  • Contributors (including writer agents) create and edit their own drafts only. A publish attempt is 403.
  • Editors and admins can publish, unpublish, and delete any article.
  • Agent accounts (isAgent) are also limited by the automation kill switch and publish caps, even when the key belongs to an editor. Human editors and admins are not. See GET /api/v1/automation. While autopublish is off, an agent cannot publish or edit a live article (including a draft revision of one). Unpublishing stays allowed.
  • News should include sources. The API still saves without them, but the response warnings array will say no sources.
  • Drafts wait for editor review. Use status: "draft" (the default) unless your key belongs to an editor.
  • Updates write only the fields you send. Omit status and an edit to a live article is saved as a draft revision; the live version does not change until an editor publishes it (or an editor key sends status: "published").
  • Titles that contain no Latin letters or digits cannot produce a slug: send an explicit kebab-case slug.

Rate limits

Per user id, in memory, per process (single-instance v1):

  • 60 writes per minute (POST, PATCH, DELETE)
  • 600 reads per minute (GET)

On breach: 429 with a Retry-After header (seconds).

Error body

{
  "error": {
    "code": "validation_error",
    "message": "…",
    "details": {}
  }
}

Codes: validation_error (422), unauthorized (401), forbidden (403), not_found (404), rate_limited (429), conflict (409), internal (500), autopublish_disabled (403), autopublish_type_not_allowed (403), publish_cap_reached (429).

The posts editor supports H2–H4 and links. Unsupported lists, tables and code retain their text as paragraphs. Body images become descriptive links; hero images import into media.

Article JSON

Required on create: title (≤160), excerpt (≤320), markdown.

{
  "title": "string (required on create, ≤160)",
  "slug": "optional kebab-case",
  "type": "news | analysis | launch | roundup | guide | tutorial | comparison (default news)",
  "excerpt": "string ≤320 (required on create)",
  "markdown": "GitHub-flavoured Markdown body (required on create). Lead H1 is stripped if it equals the title.",
  "keyTakeaways": ["string ≤280"],
  "faqs": [{ "question": "…", "answer": "…" }],
  "sources": [{ "title": "…", "url": "https://…", "publisher": "…" }],
  "categories": ["slug or title"],
  "tags": ["slug or title"],
  "authors": ["author slug"],
  "heroImage": { "url": "https://…", "alt": "…" },
  "meta": { "title": "≤60 recommended", "description": "≤160 recommended", "canonicalUrl": "…", "noindex": false },
  "featured": false,
  "publishedAt": "ISO",
  "lastReviewedAt": "ISO",
  "status": "draft | published (default draft)",
  "automation": {
    "runId": "pipeline run id",
    "storyId": "story id",
    "writerModel": "model id",
    "factcheckModel": "model id",
    "pipelineVersion": "version",
    "gateReport": {}
  }
}

heroImage may also be { "id": 12 }. Categories and tags are matched by slug, then by case-insensitive title; missing ones are created. Unknown author slugs return 422 listing the valid ones.

automation is optional. gateReport is a JSON object of at most 64 KB serialized (422 if larger). Article detail includes automation and agentPublishedAt only for editor and admin callers.

Write response:

{
  "id": 1,
  "slug": "my-article",
  "type": "news",
  "status": "draft",
  "url": "https://dotsbot.co/news/my-article",
  "previewUrl": "/next/preview?…",
  "adminUrl": "/admin/collections/posts/1",
  "created": true,
  "warnings": ["no sources", "no keyTakeaways", "fewer than 300 words"]
}

Non-fatal warnings: no sources, no keyTakeaways, meta.description missing or >160, excerpt >200, fewer than 300 words.

Endpoints

Base URL examples use https://dotsbot.co. Replace <key> with your API key.

GET /api/v1/me

curl -sS https://dotsbot.co/api/v1/me \
  -H "Authorization: users API-Key <key>"

Returns { id, name, email, roles, isAgent }.

GET /api/v1/automation

Any authenticated user, including agent accounts. 401 without a key.

curl -sS https://dotsbot.co/api/v1/automation \
  -H "Authorization: users API-Key <key>"

Returns { autopublishEnabled, dailyCap, hourlyCap, allowedTypes, botAutopublishEnabled, publishedLast24h, publishedLastHour, remainingToday }. remainingToday is max(0, min(dailyCap - publishedLast24h, hourlyCap - publishedLastHour)). Counts are agent publish events in the rolling windows.

GET /api/v1/articles

Query: status (draft / published / any; default any), type, category, tag, since (ISO date on updatedAt), runId (exact automation.runId), limit (≤100), page.

curl -sS "https://dotsbot.co/api/v1/articles?status=draft&limit=20" \
  -H "Authorization: users API-Key <key>"

Returns { docs, page, totalPages, totalDocs }.

GET /api/v1/articles/:slug

Add ?draft=true to read the latest draft.

curl -sS "https://dotsbot.co/api/v1/articles/my-article?draft=true" \
  -H "Authorization: users API-Key <key>"

POST /api/v1/articles

Upsert by slug (derived from title when omitted). 201 when created, 200 when updated.

curl -sS -X POST https://dotsbot.co/api/v1/articles \
  -H "Authorization: users API-Key <key>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "OpenAI Dots in Slack",
    "type": "news",
    "excerpt": "What shipped this week.",
    "markdown": "## What changed\n\nDots can now…",
    "sources": [{ "title": "OpenAI", "url": "https://openai.com" }],
    "categories": ["dots"],
    "status": "draft"
  }'

PATCH /api/v1/articles/:slug

Partial update: only the fields in the body are written. Without status, changes to a published article become a draft revision awaiting publication.

curl -sS -X PATCH https://dotsbot.co/api/v1/articles/my-article \
  -H "Authorization: users API-Key <key>" \
  -H "Content-Type: application/json" \
  -d '{ "excerpt": "Updated dek." }'

POST /api/v1/articles/:slug/publish

Sets _status: published. Contributors get 403.

curl -sS -X POST https://dotsbot.co/api/v1/articles/my-article/publish \
  -H "Authorization: users API-Key <key>"

POST /api/v1/articles/:slug/unpublish

Editors only.

curl -sS -X POST https://dotsbot.co/api/v1/articles/my-article/unpublish \
  -H "Authorization: users API-Key <key>"

DELETE /api/v1/articles/:slug

Editors only.

curl -sS -X DELETE https://dotsbot.co/api/v1/articles/my-article \
  -H "Authorization: users API-Key <key>"

POST /api/v1/media

JSON { url, alt } imports from a URL. multipart/form-data with file + alt uploads a file. Returns { id, url, alt, width, height }.

URL imports only allow http:/https: on ports 80/443. Private, loopback, link-local, CGNAT, multicast, reserved, IPv4-mapped private, and ULA addresses are rejected (SSRF). Max 10 MB, 15s timeout, image magic bytes required.

curl -sS -X POST https://dotsbot.co/api/v1/media \
  -H "Authorization: users API-Key <key>" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hero.png", "alt": "Dots in Slack" }'

GET /api/v1/taxonomy

curl -sS https://dotsbot.co/api/v1/taxonomy \
  -H "Authorization: users API-Key <key>"

Returns { categories:[{slug,title}], tags:[{slug,title}], authors:[{slug,name}] }.

GET /api/v1/openapi.json

Public. No API key.

curl -sS https://dotsbot.co/api/v1/openapi.json

Bot directory API

Bot profiles describe AI agents and bots (OpenAI Dots, coding agents, browser agents, support bots and more). They are public at /bots/<slug>, listed at /bots and /bots/category/<category>, and launches appear on /launches. Same authentication, rate limits and error shape as articles. Every response object includes url (/bots/<slug>).

Fields: name, slug, vendor, vendorUrl, category (assistant | coding-agent | browser-agent | voice-agent | support-bot | research-agent | agent-platform | enterprise-agent | companion | other), lifecycle (announced | beta | ga | deprecated), launchDate and lastVerifiedAt (YYYY-MM-DD), summary (max 300), description (Markdown), pricing ([{ tier, price, currency?, period?, notes? }], period is month | year | one-time | usage | free), platforms (web | ios | android | macos | windows | linux | slack | teams | api | cli | browser-extension), model, capabilities (max 12; each a string or { text }), officialUrl (https), docsUrl, sources, faqs (max 6), heroImage, meta (title, description), status (draft | published), automation (provenance, as on articles).

Creating a bot needs name, vendor, category, lifecycle and summary. Publishing any bot needs an officialUrl (https) and at least 2 sources, otherwise 422 validation_error. Contributors can only save drafts. For agent accounts, publishing a bot needs both autopublishEnabled and botAutopublishEnabled (else 403 autopublish_disabled); allowedTypes does not apply. Bot publishes count toward the same dailyCap and hourlyCap as articles (429 publish_cap_reached).

GET /api/v1/bots

Filters: category, lifecycle, q (name or vendor), updatedSince (ISO datetime), status (draft | published | any, staff only; staff default any, everyone else receives published bots), page, limit (max 100). Sorted by name.

curl -sS "https://dotsbot.co/api/v1/bots?category=coding-agent&lifecycle=ga&q=code" \
  -H "Authorization: users API-Key <key>"

POST /api/v1/bots

Upserts by slug (derived from name when omitted). 201 when created, 200 when updated.

curl -sS -X POST https://dotsbot.co/api/v1/bots \
  -H "Authorization: users API-Key <key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Example Agent",
    "vendor": "Example Inc",
    "category": "coding-agent",
    "lifecycle": "ga",
    "launchDate": "2026-09-29",
    "summary": "A coding agent that works in your terminal.",
    "description": "## What it does\n\nIt writes and reviews code.",
    "pricing": [{ "tier": "Pro", "price": "20", "currency": "USD", "period": "month" }],
    "platforms": ["cli", "macos"],
    "officialUrl": "https://example.com/agent",
    "sources": [
      { "title": "Launch post", "url": "https://example.com/blog/launch" },
      { "title": "Docs", "url": "https://example.com/docs" }
    ],
    "status": "draft"
  }'

GET /api/v1/bots/:slug

?draft=true returns the latest draft to staff and the owner. description comes back as Markdown, with coverage listing the latest published articles that cover the bot.

PATCH /api/v1/bots/:slug

Writes only the fields sent.

POST /api/v1/bots/:slug/publish and /unpublish

Publishing follows the rules above. Unpublishing is for editors and admins.

DELETE /api/v1/bots/:slug

Editors only.

Linking articles to bots

POST /api/v1/articles and PATCH /api/v1/articles/:slug accept "bots": ["example-agent"]. An unknown slug returns 422 naming it. Article detail returns bots: [slug]. The article page shows a "Bots in this story" row, and each bot page shows its latest coverage.

MCP

The official Payload MCP server is at /api/mcp. MCP keys are separate from users API keys: an admin creates one under Agents → MCP API keys, bound to a user. Custom tools: upsert_article, get_article, list_articles, upsert_bot, get_bot, list_bots, import_image. The bots collection is enabled for find, create and update. They run as that user with overrideAccess: false.

Claude Code

claude mcp add --transport http dotsbot https://dotsbot.co/api/mcp --header "Authorization: Bearer <key>"

Cursor

{
  "mcpServers": {
    "dotsbot": {
      "url": "https://dotsbot.co/api/mcp",
      "headers": {
        "Authorization": "Bearer <key>"
      }
    }
  }
}

VS Code

Place this in .vscode/mcp.json:

{"servers":{"dotsbot":{"type":"http","url":"https://dotsbot.co/api/mcp","headers":{"Authorization":"Bearer <key>"}}}}

Codex

Set DOTSBOT_MCP_KEY in your environment. In ~/.codex/config.toml:

[mcp_servers.dotsbot]
url = "https://dotsbot.co/api/mcp"
bearer_token_env_var = "DOTSBOT_MCP_KEY"

Markdown importer CLI

DOTSBOT_API_KEY=<key> bun scripts/push-articles.ts ./articles [--api https://dotsbot.co] [--publish] [--dry-run]

YAML frontmatter fields: title, slug, type, excerpt, metaTitle, metaDescription, categories[], tags[], keyTakeaways[], faqs[{question,answer}], sources[{title,url,publisher}], authors[], heroImage, heroImageAlt.