# CMS Caddie Agent API Programmatic, scope-gated access to a CMS Caddie project for autonomous agents. Each agent key is bound to a single project and carries one or more scopes. Base URL: https://agents.cmscaddie.com Discovery (JSON): https://agents.cmscaddie.com/agent-manifest OpenAPI: https://agents.cmscaddie.com/openapi.json MCP endpoint: https://agents.cmscaddie.com/mcp ## Auth Send `Authorization: Bearer ` on every request. Keys are minted from the project settings page under 'Agent API Keys' and each key is scoped to one project. ## Scopes - `content:read` — Retrieve and search content items. - `content:write` — Create and update content items. - `content:delete` — Delete content items. - `content:publish` — Set `published` to a timestamp at or before now (makes content live). - `files:write` — Request a presigned S3 upload URL. - `files:delete` — Delete a file the agent (or another agent on this project) uploaded. ## Publish semantics - published <= now_utc -> item is LIVE (auto-publishes on the next minute). - published > now_utc -> item is scheduled for that time. - published far in the future (e.g. 999901010000) -> effectively unpublished. - Any write that sets `published` to a time <= now requires the `content:publish` scope. ## Conventions - `Idempotency-Key` header on writes is strongly recommended; 24h TTL. - `X-Request-ID` echoed in responses and audit log. - `?dry_run=true` validates without writing. - Errors: `{error: {code, message, retryable, request_id, details?}}` - Success: `{data: ..., request_id}` ## Routes - `GET /projects/{project}/content/search` — scope `content:read` — Full-text search across all content groups in a project. - `GET /projects/{project}/groups` — scope `content:read` — List all content groups in a project (name, slug, description). Use this first to resolve a human-friendly group name to its id. - `GET /projects/{project}/groups/{group}/content` — scope `content:read` — List content items in a group. - `GET /projects/{project}/groups/{group}/content/{id}` — scope `content:read` — Retrieve a single content item. The `{id}` path arg accepts either the short suffix or the full compound id ("--") from content.create. - `POST /projects/{project}/groups/{group}/content` — scope `content:write` — Create a content item. REQUIRED top-level keys: `published` (string, YYYYMMDDHHMM, UTC) and `fields` (object). Draft convention: to create an unpublished draft, set `published` to the far-future sentinel `999901010000` — this keeps the item out of public feeds and does NOT require the content:publish scope. Setting `published` to a past/now time publishes immediately and requires content:publish. The `fields` object is a dict keyed by field slug; each value MUST be a `{name, type, value}` dict (NOT a bare string or number). Per-type value rules: `plain-text`/`html`/`code` → value is a string; `number` → value is a STRING-encoded number (e.g. "42" or "3.14"), not a raw int/float; `image`/`video`/`audio`/`file` → value is the S3 URL returned by files.upload, and these types MAY include an additional sibling key `alt` (string) alongside name/type/value — `alt` is the documented exception to the {name,type,value} shape and applies only to media types; `multiple-files` → value is a JSON array of URL strings; `content::` → value is the referenced item's id. Always call groups.list first to discover the field slugs, names, and types for the target group. If a field in groups.list includes a non-empty `example` string, treat it as an operator-authored format hint: match the shape, units, and precision of the example (e.g. `"$19.99"` vs `"19.99"` vs `"1999"`; `"2026-04-13"` vs `"04/13/2026"`; `"Acme, Inc."` vs `"acme-inc"`). Do NOT copy the example's literal value into the content — only mirror its format. If no `example` is present, fall back to the per-type defaults above and treat the value as freeform within the type's constraints. - `PATCH /projects/{project}/groups/{group}/content/{id}` — scope `content:write` — Merge-patch a content item. Same field schema as content.create — each entry under `fields` must be a `{name, type, value}` dict keyed by field slug. Number values are string-encoded (e.g. "42"). Media types (image/video/audio/file) may include `alt` alongside name/type/value. When updating one field, send the full `{name, type, value}` object for that field (not just the new value). Changing `published` to now-or-past (publishing, or un-drafting by moving off the '999901010000' draft sentinel to a real past/now timestamp) requires content:publish. Moving `published` BACK to '999901010000' reverts the item to an unpublished draft. The `{id}` path arg accepts either the short suffix or the full compound id ("--") returned by content.create. - `DELETE /projects/{project}/groups/{group}/content/{id}` — scope `content:delete` — Delete a content item and fire the `delete` webhook. The `{id}` path arg accepts either the short suffix (e.g. "abc12345") or the full compound id returned by content.create ("--"); pass whichever you have. Likewise `{group}` accepts a bare slug or the compound "-" form — groups.list returns both. - `POST /projects/{project}/files` — scope `files:write` — Request a presigned POST for uploading a file directly to S3. Returns {upload, s3_key, expires_in, max_bytes}. - `DELETE /projects/{project}/files` — scope `files:delete` — Delete a file under this project's agents/ prefix. s3_key must begin with 'agents/{project}/'.