# REST API

> Every /api/v1/docs route with the scope it needs, plus the concurrency rule that stops two writers overwriting each other.

The versioned REST API is at `/api/v1`. Authenticate with a bearer token.

```bash
curl -fsS https://blueprintr.io/api/v1/docs \
  -H "Authorization: Bearer $BLUEPRINTR_TOKEN"
```

## Routes

`{ref}` is a folium slug or id. **Personal keys only** means a `bpk_user_…`
key. An organisation key on those routes is refused with `403`, not a `404`.

| Route | Scope | Does |
| --- | --- | --- |
| `GET /docs` | `read:foliums` | Foliums you can author |
| `GET /docs/{ref}` | `read:foliums` | The page tree. `?status=all` includes drafts |
| `GET /docs/{ref}/pages` | `read:foliums` | Flat page list |
| `POST /docs/{ref}/pages` | `write:foliums` | Create a page |
| `GET /docs/{ref}/pages/{path}` | `read:foliums` | One page by its reader path |
| `GET /docs/{ref}/page/{pageId}` | `read:foliums` | One page by id |
| `PATCH /docs/{ref}/page/{pageId}` | `write:foliums` | Edit content or metadata |
| `DELETE /docs/{ref}/page/{pageId}` | `write:foliums` | Archive a page and its subtree |
| `POST /docs/{ref}/page/{pageId}/move` | `write:foliums` | Reparent or reorder |
| `POST /docs/{ref}/page/{pageId}/publish` | `write:foliums` | Publish or retract one page |
| `GET /docs/{ref}/search` | `read:foliums` | Search within the folium |
| `GET /docs/{ref}/diagnostics` | `read:foliums` | The last run |
| `POST /docs/{ref}/diagnostics` | `write:foliums` | Run the rules now |
| `GET /docs/{ref}/versions` | `write:foliums` | Published versions |
| `POST /docs/{ref}/versions` | `write:foliums` | Snapshot the published set as a version |

Version routes are edit-gated in both directions, including the `GET`. The list
alone tells a public reader how a document has been managed.

## Creating and editing a page

```bash
curl -fsS -X POST https://blueprintr.io/api/v1/docs/handbook/pages \
  -H "Authorization: Bearer $BLUEPRINTR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Deploying",
        "parentId": null,
        "kind": "doc",
        "content": "# Deploying\n\nOne command.",
        "description": "How a release reaches production.",
        "icon": "rocket",
        "hideInNav": false,
        "status": "draft"
      }'
```

Strings past their ceiling are **clamped rather than rejected**, matching what
the app itself does. Only shape errors come back as errors, because those are
the ones a caller can fix.

`PATCH` takes any subset of `title`, `description`, `content`, `icon` and
`hideInNav`.

> [!IMPORTANT]
> `PATCH` will not accept `status`, `kind` or `slug`.
> A content edit must never publish a draft somebody held back, as a side
> effect of one stray field. Publishing has its own route so it has its own
> audit line; a `kind` change turns a content page into a pointer; a slug
> change is a URL move, which is what `/move` is for.

## Publishing

```bash
curl -fsS -X POST \
  https://blueprintr.io/api/v1/docs/handbook/page/$PAGE_ID/publish \
  -H "Authorization: Bearer $BLUEPRINTR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"published": true}'
```

REST publishes and retracts. Its MCP twin, `update_folium_page`, accepts
`status:"draft"` only: it can retract, never publish.

A personal REST key is a human's own credential, minted behind step-up on a
password-backed account and usually driven by a CLI that human just ran. An
autonomous agent should not be what makes a draft world-readable.

`POST /versions` answers **202 Accepted** with `status:"submitted-for-review"`
when the folium requires publish approval and you are not a documentation
admin. A change request was filed, so treat it as a success.

## Deleting

`DELETE` is a **soft archive of the whole subtree**, not a hard delete. When the
page has children, the first attempt is refused and tells you how many:

```bash
curl -fsS -X DELETE \
  "https://blueprintr.io/api/v1/docs/handbook/page/$PAGE_ID?confirmSubtree=1" \
  -H "Authorization: Bearer $BLUEPRINTR_TOKEN"
```

Without `?confirmSubtree=1` a one-page delete can never take a section with it.

There is no delete tool on the MCP server at all. Deletion is offered here
because a personal key is a human's own credential and a docs CLI cannot push
one without it.

## Concurrent writes

Every write accepts `baseUpdatedAt`: the folium's `updatedAt` as you last read
it. Send it and a write that would land on top of someone else's is refused
with **409**. Omit it and the write is last-writer-wins.

`PATCH`, `POST /publish` and `POST /move` take it in the JSON body. `DELETE`
has no body, so it takes it as a query parameter:

```
DELETE /api/v1/docs/handbook/page/{id}?baseUpdatedAt=2026-09-05T23:04:11.882Z
```

Responses include both `updatedAt` (the page) and `foliumUpdatedAt` (the
document). Feed the latter back into your next write.

## Page bodies in bulk

Page bodies are served through the
[MCP server](/foliums/blueprintr-user-guide/developers/mcp-server) rather than one REST
call per page, because MCP accepts many tool calls per HTTP request. A
two-hundred-page folium is ten round trips instead of two hundred.

Both surfaces return the author's markdown **verbatim**, and neither applies
the anonymous audience filter, so a read, edit and write round trip cannot drop
an audience-gated block.

## Errors

Standard status codes, and two conventions particular to this API.

| Code | Means |
| --- | --- |
| `403` | The key kind is wrong: an org key on a personal-key route |
| `404` | Not found, **or** found and not yours. Telling you which would itself be a disclosure |
| `409` | `baseUpdatedAt` is stale. Re-read, reapply, retry |
| `202` | Accepted but not done: a version publish went to review instead |

## The CLI does all of this

[`blueprintr-docs`](/foliums/blueprintr-user-guide/foliums/docs-cli) is built on these
routes, validates offline against the server's own rules first, and refuses to
delete anything. For authoring documentation from a text editor, use it rather
than writing a client.
