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:
- Email, name, password as usual.
- Role Contributor (the default). Contributors can create and edit their own drafts; they cannot publish.
- Tick Is agent so the audit trail marks the account as automated.
- 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 agents) create and edit their own drafts only. A publish attempt is 403.
- Editors and admins can publish, unpublish, and delete any article.
- News should include sources. The API still saves without them, but the response
warningsarray will sayno 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
statusand 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 sendsstatus: "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).
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 | guide | tutorial (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)"
}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.
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/articles
Query: status (draft / published / any; default any), type, category, tag, since (ISO date on updatedAt), 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.jsonMCP
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, import_image. 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.