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