# Webharu CMS — Full Developer Reference for AI Agents (Beta) This single file contains everything needed to integrate with Webharu CMS: REST API reference, MCP server reference, authentication, error handling, and examples. Audience: AI coding agents (Claude Code, Cursor, etc.) and developers. ## Overview Webharu CMS is a Japanese-first, AI-optimized headless CMS operated by Web春 Inc. (webharu.com). It manages "posts" (articles) for client websites. When a post's status becomes `published`, the platform automatically commits the article to the site's Git repository and triggers build & deploy — the article appears on the production site within ~2-3 minutes. No webhooks or CI configuration are required from the API consumer. - Base URL: `https://cms.webharu.com` - REST API prefix: `/api/ext/v1` - MCP endpoint: `/mcp` (Streamable HTTP, stateless) - Auth (both): `Authorization: Bearer whk_...` (per-site API key, self-issued by the site owner at https://cms.webharu.com/my/ → "AIツール連携(APIキー)", max 5 per site) - All requests/responses are JSON (except image upload which is multipart/form-data) - Timestamps in responses are `YYYY-MM-DD HH:MM:SS` (UTC). Inputs accept ISO 8601 (e.g. `2026-08-01T10:00:00+09:00`). ## Authentication Every request must include: ``` Authorization: Bearer whk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json (when sending a body) ``` - One key = one site (tenant). The key can only touch that site's content. - Keys are stored server-side as SHA-256 hashes; the plaintext is shown only once at issuance. - Lost a key? It cannot be recovered — delete it at https://cms.webharu.com/my/ and issue a new one. - Disabling/deleting a key takes effect immediately (all requests return 401). - Every operation is recorded in the site's audit log with the key's name. - Store the key in an environment variable (suggested name: `WEBHARU_API_KEY`). Never commit it. ## Content model A post has these fields: | field | type | notes | |---|---|---| | id | string | system-issued, starts with `post_` | | slug | string | URL identifier, unique per site; auto-generated if omitted | | title | string | article title (aim for ~25-30 chars in Japanese) | | body_md | string | Markdown body — the source of truth. Headings start at `##` | | body_html | string | generated server-side from body_md when not provided (recommended: do not send) | | excerpt | string/null | summary (~80 chars) used in lists / OGP / search | | cover_image | string/null | image URL (use the media endpoints to obtain one) | | category | string/null | free-text category name; check existing ones via GET /categories to avoid variants | | status | string | `draft` (default) / `published` / `scheduled` | | collection | string | e.g. `news` (default), `blog`. Allowed values via GET /me | | publish_at | string/null | required when status=scheduled (ISO 8601); executed within 5 minutes of the time | | published_at | string/null | set automatically on publish | | author | string/null | display name; API operations record the key's name | | updated_at | string | last modified | | seo | object/null | per-article SEO settings (see below). Send `null` to clear; omit to keep | SEO object (all optional — include only what you set): - `meta_title` string: overrides the tag (page h1 stays = title). Aim <= ~32 full-width chars. - `meta_description` string: overrides meta description / og:description (default is excerpt). ~80-120 chars. - `noindex` boolean: true = exclude from search engines. - `canonical` string: absolute URL when the article is a re-post of another page. - `takeaways` string[]: 3-5 key points (rendered as a summary box + used for AI-search/AIO). Max 10. - `faq` [{q, a}]: FAQ pairs rendered as FAQPage structured data (JSON-LD). Max 20. - `jsonld_extra` object[]: raw JSON-LD blocks for anything else (HowTo, Product, ...). Max 5. Writing articles with AI? Fill meta_description, takeaways and faq — they are the highest-leverage fields for search and AI-answer visibility. Status semantics: - `draft`: not visible anywhere on the site. Creation defaults to draft — safe. - `published`: live. Triggers automatic build & deploy on Git-connected sites. - `scheduled`: published automatically once publish_at passes (5-minute scheduler). - Setting a published post back to `draft` removes it from the site. - Deleting a published post also removes it from the site. Irreversible. ## REST API reference All success responses: `{ "ok": true, "data": ... }`. All errors: `{ "error": "human-readable Japanese message" }` with HTTP status. ### GET /api/ext/v1/me Returns site info for the key. Use as connection test. Response data: `{ tenant_name, site_url, collections: [{key, label}] }` ### GET /api/ext/v1/posts?collection=&status=&limit= List posts, newest first. Body fields are NOT included (use GET /posts/:id). - collection: default `news` - status: filter by `draft` | `published` | `scheduled` (omit for all) - limit: default 50, max 200 Response data: array of post summaries (id, slug, title, excerpt, cover_image, category, status, collection, publish_at, published_at, author, updated_at). ### GET /api/ext/v1/posts/:id Full post including body_md/body_html. `:id` accepts either the post id or the slug. ### POST /api/ext/v1/posts Create a post. Required: `title`, `body_md`. Optional: category, excerpt, cover_image, status (default draft), publish_at (required if status=scheduled), collection (default news), slug (auto-generated if omitted), seo (see SEO object above). ```bash curl -X POST "$BASE/api/ext/v1/posts" \ -H "Authorization: Bearer $WEBHARU_API_KEY" -H "Content-Type: application/json" \ -d '{ "title": "新メニューのご案内", "body_md": "## 夏の新メニュー\n\n7月から新メニューが始まりました。", "category": "お知らせ", "excerpt": "7月からの新メニューをご紹介します" }' ``` Returns the created post in `data` (note `data.id`, `data.slug`). 409 = slug already used (choose another or omit slug). ### PUT /api/ext/v1/posts/:id Partial update — only the fields you send are changed. `:id` accepts id or slug. This is also how you publish/unpublish: ```bash # publish curl -X PUT "$BASE/api/ext/v1/posts/post_abc" -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -d '{"status":"published"}' # schedule curl -X PUT "$BASE/api/ext/v1/posts/post_abc" -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"status":"scheduled","publish_at":"2026-08-01T10:00:00+09:00"}' # unpublish (also removes from site) curl -X PUT "$BASE/api/ext/v1/posts/post_abc" -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -d '{"status":"draft"}' ``` ### DELETE /api/ext/v1/posts/:id Delete a post. If it was published, it is removed from the live site too. Irreversible. ### GET /api/ext/v1/categories Existing category names for the site: `{ "ok": true, "data": ["お知らせ", "イベント"] }`. Check before creating posts to keep category names consistent. ### POST /api/ext/v1/media Upload an image (multipart/form-data, field name `file`, image/* only, max 10 MB). Response data: `{ id, filename, url }`. Use `url` as cover_image or in Markdown `![alt](url)`. ```bash curl -X POST "$BASE/api/ext/v1/media" -H "Authorization: Bearer $KEY" -F "file=@photo.jpg" ``` ### POST /api/ext/v1/media-from-url Import an image from an external https URL (image/*, max 10 MB): body `{ "url": "https://..." }`. Useful for agents that cannot handle binary uploads. Same response as /media. ## Errors & rate limits | status | meaning | action | |---|---|---| | 400 | bad request (missing field, bad datetime, etc.) | fix per message | | 401 | invalid/disabled key or missing Bearer header | check the key | | 404 | post not found | check id/slug | | 409 | slug conflict | change slug or omit it | | 429 | rate limited | back off | | 5xx | server error | retry with exponential backoff | Rate limit: 120 requests/min per key. For batch jobs, keep to 1-2 req/sec. Retry guidance: retry 429/5xx with exponential backoff (1s, 2s, 4s...). Do not retry other 4xx. ## MCP server Remote MCP server (Model Context Protocol), Streamable HTTP transport, stateless. Endpoint: `https://cms.webharu.com/mcp` Auth: same Bearer API key, sent as an HTTP header by the MCP client config. Protocol: JSON-RPC 2.0 over POST; supports `initialize`, `ping`, `tools/list`, `tools/call`. No SSE stream (GET returns 405); clients work with plain POST responses. ### Setup Claude Code: ```bash claude mcp add --transport http webharu-cms \ https://cms.webharu.com/mcp \ --header "Authorization: Bearer whk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` Claude Desktop: Settings → Connectors → Add custom connector → URL above + header `Authorization: Bearer whk_...`. Generic mcp.json (Cursor etc.): ```json { "mcpServers": { "webharu-cms": { "url": "https://cms.webharu.com/mcp", "headers": { "Authorization": "Bearer whk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` ### Tools | tool | args | behavior | |---|---|---| | get_site_info | — | site name, URL, available collections. Call first. | | list_posts | collection?, status?, limit? | newest first | | get_post | id (id or slug) | full post incl. body_md | | create_post | title*, body_md*, category?, excerpt?, cover_image?, status?, publish_at?, collection?, slug? | default status=draft | | update_post | id* + any fields | partial update; also publish/unpublish | | delete_post | id* | removes from site if published; irreversible | | list_categories | — | existing category names | | upload_image_from_url | url* | imports https image, returns usable URL | Safety design: creation without explicit status is always a draft (the AI cannot accidentally publish); one key = one site; all operations audited; keys revoke instantly. Recommended flow: create_post (draft) → human/agent review → update_post {status: published}. ## Integration recipes 1) Publish a weekly article automatically (any language): - POST /posts with title/body_md (draft) → review → PUT {status: "published"} 2) Mirror content into another system: poll GET /posts?status=published every few minutes and diff on updated_at (no webhooks in beta). 3) Attach images: POST /media (file) or /media-from-url (https URL) → use returned url as cover_image or inline Markdown image. 4) Idempotent upserts: choose a deterministic slug and handle 409 by PUT /posts/:slug. ## Beta notes - Invite-only beta operated for Web春 clients; keys are issued by the site operator. - No extra cost during beta for contracted clients. - Roadmap candidates: webhooks, read-only key scopes, richer query filters. - Human-readable docs: https://cms.webharu.com/docs/ - Contact: your Web春 representative.