AI Docs (Magic Pad)
Create, edit, and share project-scoped documents — the same docs users see in the Kolbo app's Magic Pad editor.
AI Docs are HTML documents that live inside a project, editable in the Kolbo app's Magic Pad editor. Through the API you (or an LLM driving the MCP) author the document content directly — production plans, briefs, scripts, research summaries — and the user picks it up in the app, fully formatted and editable.
Content is HTML and is sanitized server-side against a tag allow-list before it is stored. Stick to semantic elements the editor understands: <h1>–<h6>, <p>, <br>, <ul>/<ol>/<li>, <table> (with <thead>/<tbody>/<tfoot>/<tr>/<th>/<td>/<colgroup>/<col>), <blockquote>, <code>/<pre>, <strong>/<b>, <em>/<i>, <u>, <s>/<strike>/<del>, <mark>, <sub>/<sup>, <small>, <a>, <img>, <hr>, <div>/<span>, <figure>/<figcaption>, <details>/<summary>, <label>.
What the sanitizer actually does: <script> and <style> tags, on* event handlers, and
javascript: URLs are removed. Inline style attributes are kept for a fixed list of safe CSS
properties (colors, alignment, font, spacing, borders, sizing). <iframe> is kept but only for
a fixed embed allow-list (YouTube and youtube-nocookie, Vimeo, Spotify, SoundCloud, CodePen,
CodeSandbox, Google Maps, Figma, Loom) over https. <input> survives only as a disabled
checkbox, for task lists. Every <a> is rewritten with rel="noopener noreferrer", and its href
may only be http, https, or mailto. <img src> additionally allows data: URIs, so small
inline images survive. Protocol-relative (//host/…) URLs are stripped everywhere.
Endpoint
POST /api/v1/docs
GET /api/v1/docs
GET /api/v1/docs/:id
PUT /api/v1/docs/:id
PATCH /api/v1/docs/:id/share
DELETE /api/v1/docs/:idAll six routes are limited to 120 requests per minute, keyed by authenticated user — several API keys on one account share the counter, and it is the same bucket as the media-library routes (see Errors & Limits).
On the four /docs/:id routes, a malformed id returns 400 with "Invalid doc id." rather than 404.
Create a Doc
POST /api/v1/docsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Document title. Must be a non-empty string; trimmed, then truncated to 200 characters rather than rejected. |
content | string | No | Full HTML body of the document. Must be a string when present (400 otherwise). Omitted → the doc is created empty. |
project_id | string | No | Project to create the doc in — needs edit permission or higher (see Projects). An invalid id returns 400 SDK_PROJECT_INVALID_ID; one you cannot write to returns 403 SDK_PROJECT_ACCESS_DENIED or 404 SDK_PROJECT_NOT_FOUND. Omitted → your auto-created "API Generations" project, created on the spot if it does not exist yet. |
Any other field in the body is ignored. Creating a doc also bumps the parent project's updated-at timestamp so it surfaces at the top of the project list — that bump is debounced to once per 30 seconds per project per server process, so a burst of creates only moves it once. Updating a doc does not bump it.
curl -X POST https://api.kolbo.ai/api/v1/docs \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Production Plan",
"content": "<h1>Production Plan</h1><p>Shot list and schedule…</p>",
"project_id": "65f1c8a2e4b0a3c1d9f5e123"
}'Response (201):
{
"success": true,
"doc": {
"id": "6a53c31f49989427bdaf199b",
"title": "Production Plan",
"project_id": "65f1c8a2e4b0a3c1d9f5e123",
"is_shared": false,
"is_editable_by_link": false,
"share_url": null,
"created_at": "2026-07-12T16:38:55.871Z",
"updated_at": "2026-07-12T16:38:55.871Z"
}
}List Docs
GET /api/v1/docs| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | No | Restrict to one project. Must be a valid ObjectId (400 otherwise) and you must be a member (403 otherwise). Also accepted as projectId. |
page | number | No | 1-indexed. Default 1. |
limit | number | No | Results per page. Default 20, max 50. |
Returns metadata only (no content), most recently updated first, with pagination: { page, limit, total, pages }.
Scope depends on project_id. With project_id, you get every member's docs in that shared
project, and each row carries owner ({ userId, name, avatar }) plus is_mine. Without it, you
get only your own docs across all projects — a teammate's doc will not appear.
Get a Doc
GET /api/v1/docs/:idReturns the doc metadata plus its full HTML content, and always includes owner / is_mine. Readable by the owner and by any member of the doc's project. An id you cannot access returns 404 (existence is never leaked).
Update a Doc
PUT /api/v1/docs/:id| Field | Type | Required | Description |
|---|---|---|---|
title | string | No* | New title. Must be a non-empty string; truncated to 200 characters. |
content | string | No* | Full replacement HTML — replaces the whole document. Read first, apply edits, send the complete result. |
* At least one of title / content must be present, otherwise 400. A title that is present but empty or non-string is 400; a non-string content is 400.
Writable by the owner and by project members with edit permission or higher. A doc you cannot write to returns 404, not 403 — existence is never leaked. Note the response is the metadata shape only; it does not echo content back.
Updating a doc does not move it between projects. There is no route for that — create the doc in the right project, or move it in the app.
Share a Doc
PATCH /api/v1/docs/:id/share| Field | Type | Required | Description |
|---|---|---|---|
shared | boolean | Yes | true = publicly shared, false = private (the link stops working). Explicit set, not a toggle — a non-boolean returns 400. |
editable | boolean | No | Allow link visitors to edit the doc. Omitted → unchanged (false on a new share). |
Requires the same write access as PUT. The share token is generated once on the first share and reused afterwards, so re-sharing a doc gives back the same URL.
curl -X PATCH https://api.kolbo.ai/api/v1/docs/6a53c31f49989427bdaf199b/share \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "shared": true }'{
"success": true,
"doc": {
"id": "6a53c31f49989427bdaf199b",
"title": "Production Plan",
"project_id": "65f1c8a2e4b0a3c1d9f5e123",
"is_shared": true,
"is_editable_by_link": false,
"share_url": "https://app.kolbo.ai/shared/magicpad/6d5f837497f742393cb766b0c59f728a",
"created_at": "2026-07-12T16:38:55.871Z",
"updated_at": "2026-07-12T17:04:02.118Z"
}
}share_url is only populated while is_shared is true. Setting shared: false returns
share_url: null — the token is kept, so re-enabling restores the identical link.
Delete a Doc
DELETE /api/v1/docs/:idSoft delete, owner only — project members with edit cannot delete a teammate's doc, and get 404. Deleting an already-deleted doc also returns 404.
{ "success": true, "doc": { "id": "6a53c31f49989427bdaf199b" } }The response echoes only the id; there is no undelete route in the API.
Errors
| Status | Cause |
|---|---|
| 400 | title missing or empty on create; neither title nor content on update; content not a string; shared not a boolean; malformed doc id or project_id. |
| 403 | On create, project_id names a project you can only view (SDK_PROJECT_ACCESS_DENIED). On list, project_id names a project you are not a member of — the list route answers 403 here, while create answers 404. |
| 404 | The doc does not exist, is soft-deleted, or you lack the required access — read needs any project membership, write needs edit or higher, delete needs ownership. Also returned by create when project_id is a project you cannot see at all (SDK_PROJECT_NOT_FOUND). |
MCP Tools
The matching @kolbo/mcp tools are create_doc, list_docs, get_doc, update_doc, share_doc, and delete_doc. The intended flow for an LLM:
1. (user names a project?) list_projects -> capture id
2. create_doc { title, content: "<full HTML you authored>", project_id }
3. (user wants a link?) share_doc { doc_id, shared: true } -> give share_url to the usercreate_doc makes content required, which is stricter than the HTTP route — over MCP you
cannot create an empty doc. Its inline guidance also tells the model that <iframe> is stripped;
in reality the sanitizer keeps allow-listed https embeds, so a YouTube or Figma embed you write
will survive.