# DotsBot API for agents & developers

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](/api/v1/openapi.json). This page as Markdown: [/developers.md](/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 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 `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

```json
{
  "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`.

```json
{
  "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:

```json
{
  "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

```bash
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`.

```bash
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.

```bash
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.

```bash
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.

```bash
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**.

```bash
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.

```bash
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.

```bash
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.

```bash
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

```bash
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.

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

## 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`, `import_image`. They run as that user with `overrideAccess: false`.

### Claude Code

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

### Cursor

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

### VS Code

Place this in .vscode/mcp.json:

```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`:

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

## Markdown importer CLI

```bash
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`.
