# BASE REST API Manual (for LLMs)

> This document is a reference for LLMs / AI assistants to operate a BASE knowledge base via the REST API.
> For the human-oriented introductory guide, see `/guide/api`.
>
> - **Target API version**: 1.1.0 (OpenAPI 3.1)
> - **This manual**: `https://base.kodama.com/api/manual.md` (the same content is also served at `https://base.kodama.com/llms-full.txt`)
> - **OpenAPI schema**: `https://base.kodama.com/api/openapi.yaml`
> - **Base URL**: `https://base.kodama.com/api/v1`

## 1. Overview

BASE is a hierarchical knowledge-base system. Via the REST API you can list / read / search pages and fetch the tree (GET), and create / partially update / soft-delete pages (CRUD).

**Characteristics**:
- Stateless (every request is authenticated independently)
- JSON in / JSON out
- UTC ISO 8601 timestamps
- One token is locked to one Base (cross-Base access is forbidden)

## 2. Authentication

### Bearer Token

Every request must include:

```
Authorization: Bearer kid_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Tokens are issued from the KID (Kodama ID, `https://id.kodama.com`) API token page.

### Scope model

| Scope | Allowed operations |
|-------|-------------------|
| `read` | GET only (list, detail, search, tree, Base info) |
| `read` + `write` | GET + POST / PATCH / DELETE (all CRUD) |

**Custom scope**: a target Base ID must be specified at issue time. The token is locked to that single Base; requests for other Bases get `403 FORBIDDEN` (data-leak prevention).

### Permission inside the Base

| API scope | Required Base role |
|-----------|--------------------|
| GET | `owner` only (reader / writer cannot use the API) |
| POST / PATCH / DELETE | `owner` only (plus `write` scope) |

All API access (read and write) is restricted to the Base **owner**. Members joined as `reader` or `writer` cannot use the API; they get `403 FORBIDDEN`.

## 3. Endpoint list

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/bases/{base_id}/info` | Base summary |
| `GET` | `/bases/{base_id}/pages` | Page list |
| `GET` | `/bases/{base_id}/pages/{content_id}` | Page detail |
| `GET` | `/bases/{base_id}/search?q={keywords}` | Search |
| `GET` | `/bases/{base_id}/tree` | Page tree |
| `POST` | `/bases/{base_id}/pages` | Create page |
| `PATCH` | `/bases/{base_id}/pages/{content_id}` | Partial update |
| `DELETE` | `/bases/{base_id}/pages/{content_id}` | Soft delete |

**Path parameters**:
- `base_id`: lowercase letters + digits + hyphen, 6 chars or more
- `content_id`: alphanumeric, exactly 6 chars

## 4. Data model

### Page (detail)

```json
{
  "content_id": "9y6f4u",
  "title": "Page title",
  "body": "# Body\n\nMarkdown source...",
  "type": "M",
  "level": 0,
  "path": ["Parent title", "Page title"],
  "lang": "en",
  "created_at": "2026-05-26T01:34:10Z",
  "updated_at": "2026-05-26T01:34:27Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `content_id` | string | Content ID (6 alphanumeric chars, immutable) |
| `title` | string | Page title |
| `body` | string | Body content (Markdown source if type=M) |
| `type` | string | `T`=Text / `H`=HTML / `M`=Markdown |
| `level` | integer | Hierarchy depth (0=root) |
| `path` | string[] | Array of titles from root to this page |
| `lang` | string | `ja` / `en` |
| `created_at` | string | UTC ISO 8601 |
| `updated_at` | string | UTC ISO 8601 |

### PageSummary (for listings)

```json
{
  "content_id": "9y6f4u",
  "title": "Page title",
  "type": "M",
  "level": 0,
  "order": 5,
  "lang": "en",
  "updated_at": "2026-05-26T01:34:27Z"
}
```

`order` is the display order inside the Base (lower comes first).

## 5. GET endpoints

### 5.1 GET /bases/{base_id}/info

Base metadata.

```bash
curl -H "Authorization: Bearer $TOKEN" \
  https://base.kodama.com/api/v1/bases/my-knowledge/info
```

**Response**:

```json
{
  "base_id": "my-knowledge",
  "description": "Description of the Base",
  "view": "L",
  "total_pages": 42
}
```

`view`: `O`=public / `L`=member-only / `I`=logged-in users only.

### 5.2 GET /bases/{base_id}/pages

Page list (in display order). Array of `PageSummary`.

```json
{
  "pages": [{ "content_id": "...", "title": "...", ... }],
  "total": 42
}
```

### 5.3 GET /bases/{base_id}/pages/{content_id}

Page detail. Returns `Page` (see §4).

### 5.4 GET /bases/{base_id}/search?q={keywords}

Partial-match search over title and body. Title matches rank first, then by descending update time.

```json
{
  "pages": [{ "content_id": "...", "title": "...", ... }],
  "total": 3,
  "keywords": "search term"
}
```

### 5.5 GET /bases/{base_id}/tree

Returns the hierarchy as nested JSON (up to 3 levels).

```json
{
  "tree": [
    {
      "content_id": "abc123",
      "title": "Root page",
      "level": 0,
      "children": [
        { "content_id": "def456", "title": "Child", "level": 1, "children": [] }
      ]
    }
  ]
}
```

## 6. POST /bases/{base_id}/pages (create page)

Appends a new page at the end of the Base (`level=0`, fixed).

**Required scopes**: `read` + `write`. **Required role**: `owner` only.

### Request

```bash
curl -X POST https://base.kodama.com/api/v1/bases/my-knowledge/pages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New page",
    "body": "# Body\n\nWritten in Markdown",
    "content_type": "M",
    "content_lang": "en"
  }'
```

| Field | Required | Type | Default | Allowed values |
|-------|----------|------|---------|----------------|
| `title` | Yes | string | - | 1 char or more |
| `body` | No | string | `""` | - |
| `content_type` | No | string | `"M"` | `T` / `H` / `M` |
| `content_lang` | No | string | `"ja"` | `ja` / `en` |

### Response (201 Created)

The `Location` header points to the created resource; the body is in `Page` format.

```
Location: /api/v1/bases/my-knowledge/pages/9y6f4u
```

```json
{
  "content_id": "9y6f4u",
  "title": "New page",
  "body": "# Body\n\nWritten in Markdown",
  "type": "M",
  "level": 0,
  "path": ["New page"],
  "lang": "en",
  "created_at": "2026-05-26T01:34:10Z",
  "updated_at": "2026-05-26T01:34:10Z"
}
```

### Notes

- When `content_type=M`, the server caches the rendered HTML (`content_body_html`) automatically
- To place the page in the hierarchy, change `level` with a subsequent PATCH
- `content_id` is server-generated (6 chars, lowercase + digits)

## 7. PATCH /bases/{base_id}/pages/{content_id} (partial update)

Only the fields you send are updated. **Omitted fields keep their current value.**

**Required scopes**: `read` + `write`. **Required role**: `owner` only.

### Request

```bash
curl -X PATCH https://base.kodama.com/api/v1/bases/my-knowledge/pages/9y6f4u \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated title",
    "body": "## Updated body"
  }'
```

| Field | Type | Allowed values | Description |
|-------|------|----------------|-------------|
| `title` | string | 1 char or more | - |
| `body` | string | - | - |
| `content_type` | string | `T` / `H` / `M` | - |
| `content_lang` | string | `ja` / `en` | - |
| `view` | string \| null | `O` / `L` / `I` / `null` | Per-page visibility. Send `null` explicitly to revert to Base inheritance |
| `level` | integer | 0 or more | Tree depth. Child nodes shift in sync |

**The body must contain at least one field.** `{}` returns `400 BAD_REQUEST`.

### Response (200 OK)

The updated `Page`. `path` also reflects the new hierarchy.

### Important behavior

- **Explicit `null` on `view`**: `{"view": null}` resets the page to "inherit from Base." Omitting `view` means "do not change." The two are distinguished.
- **Changing `level`**: when you change a page's level, all descendants shift by the same delta (e.g. self goes 0 → 1, children go 1 → 2).
- **Markdown HTML rebuild**: if the page ends up with `content_type=M` and you sent `body` or `content_type`, `content_body_html` is regenerated.
- **Base mismatch**: if `content_id` belongs to a different Base, the response is `404 NOT_FOUND` (information hiding).
- **Soft-deleted pages**: pages with `status='D'` return `404 NOT_FOUND`.

### Examples

| Use case | Request body |
|----------|--------------|
| Change title only | `{"title": "New title"}` |
| Rewrite body | `{"body": "## New content"}` |
| Indent one level | `{"level": 1}` |
| Publish single page | `{"view": "O"}` |
| Clear per-page visibility | `{"view": null}` |
| Convert format (H → M) | `{"content_type": "M", "body": "# Markdown"}` |

## 8. DELETE /bases/{base_id}/pages/{content_id} (soft delete)

Moves the page to the trash (`status='D'`). Not a physical delete; restorable from the web UI (`/trash`).

**Required scopes**: `read` + `write`. **Required role**: `owner` only.

### Request

```bash
curl -X DELETE https://base.kodama.com/api/v1/bases/my-knowledge/pages/9y6f4u \
  -H "Authorization: Bearer $TOKEN"
```

### Response (204 No Content)

Empty body.

### Important behavior

- If the page has children: each child moves up one level (taking the parent's slot)
- `content_order` of following pages is compacted
- **Not idempotent**: DELETE against an already-deleted page returns `404 NOT_FOUND`
- No restore API. Use the web UI to restore.

## 9. Error responses

All errors share this JSON shape:

```json
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "title is required (must be a string with at least 1 char)"
  }
}
```

### HTTP status codes

| Status | `error.code` | Typical cause |
|--------|--------------|---------------|
| 200 | - | GET / PATCH succeeded |
| 201 | - | POST succeeded |
| 204 | - | DELETE succeeded (no body) |
| 400 | `BAD_REQUEST` | JSON parse error, missing required field, invalid enum, empty PATCH body |
| 401 | `UNAUTHORIZED` | Bearer Token missing / invalid / expired |
| 403 | `FORBIDDEN` | Base scope mismatch, not the Base `owner`, or missing `write` scope on a write request |
| 404 | `NOT_FOUND` | Base / page does not exist, or operation on a soft-deleted page |
| 405 | `METHOD_NOT_ALLOWED` | Unexpected HTTP method |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | POST / PATCH without `Content-Type: application/json` |
| 500 | `INTERNAL_ERROR` | Server-side error |

## 10. Best practices for LLM use

### 10.1 Check before creating

Before creating a new page, search for a same-title page:

```
1. GET /search?q={title} to check for duplicates
2. If none, POST /pages to create
```

Note: BASE does **not** enforce title uniqueness at the database level. This check exists to prevent autonomous LLM agents from creating duplicate pages on retry loops; treat it as a soft idempotency guard.

### 10.2 Bulk updates

PATCH is one request per page. To update many pages, call sequentially. Parallel requests are allowed (but concurrent PATCHes against the same page are last-write-wins).

### 10.3 Hierarchy operations

Understand the tree before acting:

```
1. GET /tree to inspect current shape
2. POST /pages to append at the tail (level=0)
3. PATCH /pages/{id} to set the final level
```

### 10.4 Confirm before deleting

`DELETE` is not idempotent. Calling `GET /pages/{id}` first is safer (especially when you need to distinguish "never existed" from "already deleted" - both return 404).

### 10.5 Body format

- **Prefer Markdown**: `content_type: "M"`. LLMs read and write it well, and the server caches the rendered HTML.
- HTML input is sanitized via `Parsedown SafeMode`-equivalent (script tags etc. stripped).
- Line breaks: in Markdown, plain newlines render as `<br>` (no need for trailing two spaces).

### 10.6 Error-handling guidance

| Situation | Action |
|-----------|--------|
| 401 / 403 | Check the token. Ask the user to re-issue. |
| 404 | Resource missing or deleted. Re-fetch the list to confirm. |
| 400 | Validation error. Surface `message` to the user. |
| 415 | Always send `Content-Type: application/json`. |
| 5xx | Server error. Retry with backoff. |

## 11. Full curl examples

### 11.1 Create -> reparent -> publish -> delete

```bash
TOKEN="kid_xxxxxxxx"
BASE="my-knowledge"
API="https://base.kodama.com/api/v1/bases/$BASE"

# 1. Create page
RESP=$(curl -sS -X POST "$API/pages" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"API spec notes","body":"# Spec\n\n## Auth","content_type":"M"}')
CID=$(echo "$RESP" | jq -r .content_id)
echo "Created: $CID"

# 2. Move to level=1
curl -sS -X PATCH "$API/pages/$CID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"level":1}' > /dev/null

# 3. Publish this single page
curl -sS -X PATCH "$API/pages/$CID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"view":"O"}' > /dev/null

# 4. Append to body
curl -sS -X PATCH "$API/pages/$CID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body":"# Spec\n\n## Auth\n\n## Errors"}' > /dev/null

# 5. Verify
curl -sS -H "Authorization: Bearer $TOKEN" "$API/pages/$CID" | jq

# 6. Clean up
curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" "$API/pages/$CID"
```

### 11.2 Search -> fetch matching page

```bash
# Search
curl -sS -H "Authorization: Bearer $TOKEN" "$API/search?q=$(jq -rn --arg q 'auth' '$q|@uri')"

# Fetch the body of a hit
curl -sS -H "Authorization: Bearer $TOKEN" "$API/pages/abc123"
```

## 12. Constraints and notes

- **CORS**: `Access-Control-Allow-Origin: *` (callable from browsers as well)
- **Rate limits**: none today (planned for the future)
- **Relation to MCP**: MCP (Model Context Protocol) is read-only. Writes are REST API only.
- **Body size limit**: nginx / PHP-FPM defaults (no per-endpoint cap)
- **Time zone**: input and output are UTC ISO 8601. Convert to local time on the client.
- **Not yet implemented** (future work): page reordering (changing `order`), trash operations, Base settings, user/role management, ETag-based optimistic locking, webhooks.
