Kolbo.AIKolbo.AI Docs
Developer API

Projects

Create and manage projects, feed them a knowledge base, and drop SDK generations into a specific project instead of the auto-created "API Generations" bucket.

Everything in Kolbo lives inside a project — sessions, generations, media, and AI Docs are all project-scoped. By default every generation made through the SDK lands in an auto-created project called "API Generations", so every generation endpoint accepts an optional project_id body field that routes the result into the project of your choice.

Endpoint

GET    /api/v1/projects
GET    /api/v1/projects/:id
GET    /api/v1/project/lightweight
POST   /api/v1/projects
PUT    /api/v1/projects/:id
PUT    /api/v1/projects/:id/archive
PUT    /api/v1/projects/:id/unarchive

POST   /api/v1/projects/:projectId/context/url
POST   /api/v1/projects/:projectId/context/text
GET    /api/v1/projects/:projectId/context
DELETE /api/v1/projects/:projectId/context/:fileKey
GET    /api/v1/projects/:projectId/profile
POST   /api/v1/projects/:projectId/profile/regenerate

GET    /api/v1/projects/:projectId/assets
POST   /api/v1/projects/:projectId/assets/link
PUT    /api/v1/projects/:projectId/assets/:assetType/:assetId/note
DELETE /api/v1/projects/:projectId/assets/:assetType/:assetId

PATCH  /api/v1/sessions/:sessionId/project

Deletion is intentionally not exposed. Deleting a project cascades a soft-delete across every session, generation, and media item inside it, so it stays an in-app, human-confirmed action. archive is the reversible API equivalent.

Listing Projects

The API has no concept of project names — only ObjectIds. Call GET /v1/projects to discover the ID for any project the API key's user can write to (owned, or shared with edit / full / owner permission).

ParameterTypeRequiredDescription
searchstringNoCase-insensitive substring match on the project name. Regex-escaped server-side, so user text is safe to pass through. This is the cheap way to resolve one known name.
pagenumberNo1-indexed page number. Default 1.
limitnumberNoResults per page, 1200. Default 50.
include_archivedstringNoThe literal string "true" also returns archived projects. Anything else (including omitted) excludes them.
curl "https://api.kolbo.ai/api/v1/projects?search=acme" \
  -H "X-API-Key: YOUR_API_KEY"

Response:

{
  "success": true,
  "projects": [
    { "id": "65f1c8a2e4b0a3c1d9f5e123", "name": "Acme Campaign", "description": "Q3 launch film. Tone: warm, documentary…", "role": "owner",  "is_default": false, "is_archived": false },
    { "id": "65f1c8a2e4b0a3c1d9f5e456", "name": "Shared Brand Kit", "role": "edit",  "is_default": false, "is_archived": false },
    { "id": "65f1c8a2e4b0a3c1d9f5e789", "name": "API Generations", "role": "owner", "is_default": true,  "is_archived": false }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 3, "returned": 3, "has_more": false }
}
FieldTypeNotes
idstringProject ObjectId — this is what you pass as project_id.
namestringProject name.
descriptionstring | nullUser-written brief, clipped to ~400 characters in the list. Call GET /v1/projects/:id for the full text.
rolestringowner | full | edit.
is_defaultbooleantrue only for the project literally named API Generations that you own — the bucket SDK calls fall into when project_id is omitted.
is_archivedbooleantrue when the project has been archived. Only ever true if you asked for include_archived=true.

Sorted by last-updated first. Archived projects are hidden by default — same rule as the web app. Auto-created review projects are still included here (unlike GET /project/lightweight).

GET /project/lightweight

A richer, searchable list that returns the raw in-app project shape. Useful when you need each project's description, archive state, share state, or lastOpenedAt.

ParameterTypeRequiredDescription
querystringNoCase-insensitive name filter. Regex-escaped server-side, so user text is safe to pass through.
sortBystringNodate | created_at | updated_at | name. date and created_at both sort on the creation timestamp. Omitted → unsorted.
sortDirectionstringNodesc for descending; any other value (including omitted) sorts ascending.
includeArchivedstringNoThe literal string "true" includes archived projects. Anything else excludes them.

Each row carries _id, name, description, user, owners, createdAt, updatedAt, lastOpenedAt, isShared, isArchived, and sharedUsers — nothing else is projected.

curl "https://api.kolbo.ai/api/v1/project/lightweight?query=acme&sortBy=updated_at&sortDirection=desc" \
  -H "X-API-Key: YOUR_API_KEY"

This route is a passthrough to the in-app controller, so it returns the app envelope { "status": true, "data": [ … ] }not the { "success": true, "projects": [ … ] } shape of GET /v1/projects. It also has no pagination, no per-endpoint rate limiter, and auto-created review projects are hidden. Unlike GET /v1/projects it does include view-only shares, and it also matches projects that list you in owners. Prefer GET /v1/projects for the id lookup you need before a generation.

Using project_id

Add project_id to the body of any generation endpoint:

curl -X POST https://api.kolbo.ai/api/v1/generate/image \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A neon-lit alleyway at midnight",
    "project_id": "65f1c8a2e4b0a3c1d9f5e123"
  }'

The generation will appear in that project in the web app, mixed with sessions created from the UI.

project_id is per-call, not sticky — there is no server-side "current project". Once you are working inside a named project, pass its id on every generation, upload, doc, and chat call; any call that omits it falls back to "API Generations".

Creating a Project

POST /api/v1/projects
FieldTypeRequiredDescription
namestringYesProject name. Must be a non-empty string; trimmed server-side. No length cap on this route.
descriptionstringNoProject brief. Must be a string when present. Max 10,000 characters — that ceiling comes from the database schema, so an over-long value fails validation rather than being truncated. Markdown accepted; it feeds the project's AI profile.

Any other field in the body is discarded — the handler rebuilds the request body from name and description only.

curl -X POST https://api.kolbo.ai/api/v1/projects \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Campaign",
    "description": "Q3 launch film. Tone: warm, documentary, handheld."
  }'

Response:

{
  "success": true,
  "project": {
    "id": "65f1c8a2e4b0a3c1d9f5e123",
    "name": "Acme Campaign",
    "description": "Q3 launch film. Tone: warm, documentary, handheld.",
    "is_archived": false,
    "created_at": "2026-07-12T16:38:55.871Z",
    "updated_at": "2026-07-12T16:38:55.871Z"
  }
}

Project creation is limited to 10 requests per minute per user, and your plan's project cap is enforced — exceeding it returns 403 with code: "PROJECT_LIMIT_EXCEEDED" and a data object containing current, limit, and upgradeRequired. That response is emitted by the limit middleware, so it uses the app envelope ("status": false, "message") rather than the SDK's "success": false / "error" shape.

Get one project

GET /api/v1/projects/:id

Returns the full project record, including the unclipped description. Use this before PUT /v1/projects/:id when you need to edit the brief. Same write-access rule as the list (owned or shared with edit / full / owner). MCP: get_project.

{
  "success": true,
  "project": {
    "id": "65f1c8a2e4b0a3c1d9f5e123",
    "name": "Acme Campaign",
    "description": "Q3 launch film. Tone: warm, documentary, handheld.",
    "role": "owner",
    "is_default": false,
    "is_archived": false,
    "thumbnail_url": null,
    "created_at": "2026-07-12T16:38:55.871Z",
    "updated_at": "2026-07-12T16:38:55.871Z"
  }
}

Updating, Archiving, Unarchiving

PUT /api/v1/projects/:id
FieldTypeRequiredDescription
namestringNoNew name.
descriptionstringNoNew description — replaces the old one. Max 10,000 characters.

At least one of name / description must be present, otherwise 400. Any other field in the body is dropped before the update runs. Requires edit permission or higher on the project. Changing the description to a different value schedules a (debounced) rebuild of the project's AI profile; renaming alone does not.

There is no per-endpoint rate limiter on update, archive, or unarchive — only the global 5,000/min ceiling described in Errors & Limits.

PUT /api/v1/projects/:id/archive
PUT /api/v1/projects/:id/unarchive

Both take an empty body and require full permission or ownershipedit is not enough. Archiving hides the project from the app's active list without deleting anything, and is fully reversible.

All three routes answer with the same { "success": true, "project": { id, name, description, is_archived, created_at, updated_at } } shape as create.

Non-members get 404 on all three routes (existence is never leaked), as does a malformed :id; members with an insufficient permission tier get 403.

Moving a Session Between Projects

If a session ended up in the wrong project (usually the "API Generations" default), move it — together with all of its media library items — instead of regenerating:

PATCH /api/v1/sessions/:sessionId/project
FieldTypeRequiredDescription
project_idstringYesTarget project ObjectId. You need edit / full / owner permission on it. Also accepted as newProjectId.
typestringNoSession type hint to speed up the lookup. Must be one of the 17 session types listed in Agents & Sessions; an unknown value returns 400. Omit if unsure — all types are probed.

Works for any session type: generation sessions (the session_id returned on every generation submit), chat conversations, transcription sessions, and so on. You need edit / full / owner permission on both the source and the target project — a member of a shared project can move a teammate's session. Creative Director sessions are the one exception: they own nested child sessions and return 400 UNSUPPORTED_TOOL.

The move is transactional. The session, every generation's project reference, and every media library item linked to the session move together or not at all.

curl -X PATCH https://api.kolbo.ai/api/v1/sessions/65f1c8a2e4b0a3c1d9f5e999/project \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "project_id": "65f1c8a2e4b0a3c1d9f5e123" }'

Response:

{
  "success": true,
  "session": {
    "id": "65f1c8a2e4b0a3c1d9f5e999",
    "name": "API Image Generation - 2026-07-12",
    "project_id": "65f1c8a2e4b0a3c1d9f5e123",
    "previous_project_id": "65f1c8a2e4b0a3c1d9f5e789",
    "moved_media_count": 3,
    "moved_generations_count": 2
  }
}

moved_media_count is the number of media library items that moved with the session; moved_generations_count is the number of generations whose project reference was rewritten. name is null when the session was never named. Moving a session to the project it is already in is a no-op that returns success with both counts at 0 and project_id === previous_project_id.

A successful move also drops the session from the SDK's daily-session cache, so the next generation of that type does not silently reuse the session you just relocated.

This endpoint is limited to 20 requests per minute, keyed by authenticated user — several API keys on one account share the counter, and it is the same bucket as the bulk media operations (see Errors & Limits).

To move individual media items (rather than a whole session), see the media move endpoints in Media Library: PATCH /v1/media/:id/project, POST /v1/media/bulk/move, and POST /v1/media/folders/:id/move-contents.

To enumerate sessions before moving them, see Agents & Sessions.

Moving Many Sessions at Once

Reorganizing a library one session at a time runs into the 20/min limit fast. Move up to 100 sessions in a single call instead — types may be mixed:

POST /api/v1/sessions/move
FieldTypeRequiredDescription
session_idsstring[]YesUp to 100 session ObjectIds. Mixed types are fine. Also accepted as sessionIds.
project_idstringYesTarget project ObjectId; you need edit+ on it.
typestringNoSession type hint, only useful when every id in the batch is the same type. Omit for mixed batches.

Each session moves in its own transaction, so one that cannot move never rolls back the others:

{
  "success": true,
  "project_id": "65f1c8a2e4b0a3c1d9f5e123",
  "moved_sessions_count": 38,
  "moved_generations_count": 204,
  "moved_media_count": 511,
  "operation_ids": ["65f1a01", "65f1a02"],
  "skipped": [
    { "session_id": "65f1b01", "code": "SAME_PROJECT", "message": "That session is already in this project." }
  ],
  "failed": [
    { "session_id": "65f1c01", "code": "GENERATIONS_IN_FLIGHT", "message": "1 generation(s) are still running. Wait for them to finish before moving them." }
  ]
}

skipped holds sessions that were already in the target project (nothing to do). failed holds real problems — a running generation, a session type that cannot be moved, or one you lack access to. Always surface failed to the user: a 200 here does not mean everything moved.

operation_ids carries one id per moved session; see Undoing a Reorganization.

Reorganizing Work Between Sessions

Moving whole sessions between projects is one axis. The other is moving generations between sessions — merging scattered work, or splitting one overgrown session into several.

These three routes are available for image, image-to-video, lipsync and video-to-video sessions. Other types return 400 UNSUPPORTED_TOOL and should be moved whole with the routes above.

Listing a session's generations

GET /api/v1/sessions/:sessionId/generations

Returns each generation as a complete group — its prompt plus every output it produced. A generation is never separable from its own outputs, so you always move whole entries.

{
  "success": true,
  "session": {
    "id": "65f1999",
    "name": "Hero shots",
    "project_id": "65f1123",
    "can_edit": true,
    "total_media_count": 12
  },
  "generations": [
    {
      "id": "65f1g01",
      "prompt": "wide desert shot, golden hour",
      "status": "completed",
      "in_flight": false,
      "created_at": "2026-08-11T09:14:02.000Z",
      "output_urls": ["https://media.kolbo.ai/hero-01.png"],
      "thumbnail_url": "https://media.kolbo.ai/hero-01.png",
      "output_count": 1,
      "media_count": 1
    }
  ]
}

in_flight: true means the generation is still running and cannot be moved yet.

Moving generations into an existing session

POST /api/v1/sessions/:sessionId/generations/move
FieldTypeRequiredDescription
generation_idsstring[]YesFrom the listing above. Whole entries only.
target_session_idstringYesDestination session, same session kind. May live in another project you can edit.
typestringNoSession type hint.

Only the selected generations and their own output media move. Uploads and reference images stay with the source session, so a reference still used by generations left behind is never dragged away.

Splitting a session

POST /api/v1/sessions/:sessionId/split
FieldTypeRequiredDescription
generation_idsstring[]YesGenerations to carve out.
namestringYesName for the new session (1-120 chars).
project_idstringNoPut the new session in a different project. Defaults to the source session's project.
typestringNoSession type hint.

Creates the session and moves the generations into it in one transaction:

{
  "success": true,
  "operation_id": "65f1a09",
  "session": { "id": "65f1new", "name": "Approved takes", "project_id": "65f1123" },
  "moved_generations_count": 4,
  "moved_media_count": 9
}

Undoing a Reorganization

Every move and split returns an operation_id, reversible for 15 minutes:

POST /api/v1/sessions/organize/undo/:operationId

Permissions are re-checked on both projects at undo time — the receipt is not a capability. If the work has moved again since, the undo refuses with 409 UNDO_STATE_CHANGED rather than yanking records out of wherever they now live.

{ "success": true, "operation_id": "65f1a09", "kind": "split", "undone": true }

A batch move returns one operation_id per session; call undo once per id you want to reverse.

Project cast (Visual DNAs + moodboards)

A project's cast roster is the Visual DNAs (@Name) and moodboards (#Name) tagged onto that project. The AI cast list (chat / help / generation context) is built from this roster: each DNA's identity description plus an optional project-scoped purpose note.

This does not copy the asset. Linking only tags an existing DNA or moodboard onto the project. Unlinking removes the tag; the asset stays in the library. To change a tagged DNA's identity description, update the DNA (or use the MCP update_project_asset description field) — never unlink and relink.

GET    /api/v1/projects/:projectId/assets
POST   /api/v1/projects/:projectId/assets/link
PUT    /api/v1/projects/:projectId/assets/:assetType/:assetId/note
DELETE /api/v1/projects/:projectId/assets/:assetType/:assetId

GET requires project access. Link / note / unlink require edit.

List

Returns a flat visual_dnas[] (id, name, dna_type, thumbnail_url, description, note) and moodboards[] (id, name, thumbnail_url, summary, note).

{ "asset_type": "visual_dna", "asset_id": "6601a1b2c3d4e5f6a7b8c9d0" }

asset_type is visual_dna or moodboard. Also accepts camelCase assetType / assetId.

Purpose note

PUT …/note with { "note": "hero, use on dark backgrounds" } (max 1000 chars). Empty string clears the note. This is not the DNA's identity description — that lives on the Visual DNA (PUT /v1/visual-dna/:id with prompt_helper, or MCP update_project_asset / update_visual_dna).

Removes the tag only. MCP: unlink_project_asset.

Project Knowledge Base (Context / RAG)

Feed domain knowledge into a project — scripts, briefs, research, URLs — and the platform synthesizes a living markdown profile used to ground AI work in the project.

Add a URL source

POST /api/v1/projects/:projectId/context/url
FieldTypeRequiredDescription
urlstringYesPublic web page to fetch and analyze. Trimmed server-side, and validated against internal/private addresses — a blocked target returns 400.
titlestringNoDisplay label for the source. Defaults to the URL. For a YouTube link the video title replaces it once the transcript is fetched.

Extraction order: YouTube links use the video's captions; publicly shared Google Docs / Sheets / Slides are exported as text; anything else has its page text extracted. Only the first 60,000 characters of the extracted text are kept and indexed — anything past that is discarded (the source's stored size still reports the full extracted length). If less than 20 characters of content can be read, the source settles with an explanatory note instead of a summary (a Google file that is not shared publicly gets its own note saying so).

Add a text source

POST /api/v1/projects/:projectId/context/text
FieldTypeRequiredDescription
textstringYesRaw text (script, notes, brand facts). Also accepted as content. Trimmed, then stored for retrieval — only the first 60,000 characters are kept and indexed.
titlestringNoDisplay label. Defaults to "Note".

Both add routes require edit permission or higher, and both return immediately — analysis, embedding, and profile synthesis run in the background.

curl -X POST https://api.kolbo.ai/api/v1/projects/65f1c8a2e4b0a3c1d9f5e123/context/text \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Launch brief", "text": "The film opens on a rain-slick street…" }'

Response:

{
  "success": true,
  "source": {
    "file_key": "text-source/1752338335871-9f2c1ab4e5d6",
    "type": "text",
    "title": "Launch brief",
    "url": null,
    "summary": null,
    "status": "analyzing",
    "created_at": null
  }
}

List and delete sources

GET    /api/v1/projects/:projectId/context
DELETE /api/v1/projects/:projectId/context/:fileKey

GET takes no parameters and returns { "success": true, "sources": [ … ], "count": n }. Each source has the shape above; type is one of image, document, url, text, video, audio, and status is analyzing until a summary lands, then completed. Any project member can read.

DELETE takes the source's file_key in the path (URL-encode it). An unknown key returns 404.

Deleting a context source requires you to be the project's owner (or listed in its owners). Shared members — even with edit — get 404 from the delete route, despite the route's own middleware only checking project access.

created_at is currently always null in the SDK source shape: the stored subdocument timestamps the source as uploadedAt, and the SDK response mapper looks for createdAt / addedAt.

Project profile

GET  /api/v1/projects/:projectId/profile
POST /api/v1/projects/:projectId/profile/regenerate

The profile is the synthesized living brief the platform maintains from the project's description, its context sources, and its activity. Reading it requires any project access; regenerating requires edit or higher.

GET returns:

{
  "success": true,
  "profile": {
    "content": "## Acme Campaign\nA Q3 launch film…",
    "generated_at": null,
    "is_manually_edited": false
  }
}

content is markdown (the synthesizer trims it to 4,000 characters before storing) and is null when the project has not accumulated enough material yet. is_manually_edited is true when someone edited the profile in the app, which freezes automatic re-synthesis.

POST …/profile/regenerate clears that manual-edit lock and forces a fresh synthesis. It runs synchronously and can take several seconds. It responds with { "success": true, "profile": { "content": …, "regenerated": true } }, and still returns 200 when there is not enough project material to build a profile at all.

Two fields are currently always null regardless of the underlying data: profile.generated_at on GET …/profile (the stored timestamp is lastSynthesized, which the SDK mapper does not read), and profile.content on POST …/profile/regenerate. regenerated: true is a constant, not a signal that anything changed. After regenerating, read the new profile back with GET …/profile.

Editing the profile by hand is not exposed over the API — that stays an in-app action.

Errors

StatusCodeCause
400SDK_PROJECT_INVALID_IDproject_id is not a valid ObjectId.
403SDK_PROJECT_ACCESS_DENIEDYou have view permission on the project but not edit or higher.
404SDK_PROJECT_NOT_FOUNDThe project does not exist, is soft-deleted, or you have no access at all. (We return 404 for both cases so the API does not leak project existence.)
403PROJECT_LIMIT_EXCEEDEDYour plan's project cap is reached.
400(Create) name missing or not a non-empty string; description not a string. (Update) neither name nor description supplied.
403(Archive / unarchive) you are a member but below the required full tier.
400(Session move) sessionId is not a valid ObjectId, project_id is missing/invalid, or type is not one of the 17 session keys — the error message lists the valid keys.
404(Session move) the session does not exist or is not owned by you.
400(Context) url missing on the URL route, or neither text nor content on the text route. A URL that resolves to a private/internal address is also rejected here.

MCP Tools

If you are using Kolbo through the @kolbo/mcp server (Claude Desktop, Claude Code, claude.ai connector), the matching tools are list_projects, get_project, create_project, update_project, archive_project, unarchive_project, move_session, list_project_assets, link_project_asset, unlink_project_asset, update_project_asset, add_project_context, list_project_context, delete_project_context, get_project_profile, and regenerate_project_profile. Every generate_* tool accepts an optional project_id arg whose value comes from list_projects.

add_project_context is one tool over both add routes — pass exactly one of url / text and it picks the endpoint for you. Its title arg is only forwarded on a text source; to title a URL source, call POST …/context/url over HTTP.

list_projects and create_project add an open_url field to their output that the HTTP API does not return — a client-side deep link of the form https://app.kolbo.ai/media?project=<id>. It is omitted for the is_default "API Generations" bucket.

No MCP tool calls GET /api/v1/project/lightweightlist_projects is the project-discovery tool. The lightweight list is reachable only over HTTP; there is no App Builder project-picker tool on the MCP server (App Builder is HTTP-only, as noted in the overview).

1. list_projects
2. pick the project the user named -> capture its `id`
3. generate_image  (or any other generate_* tool)
   { prompt: "...", project_id: "<id from step 2>" }

When the user does not mention a project, omit project_id and the generation lands in the user's "API Generations" default. If something landed in the wrong project, move_session relocates a whole session (plus its media), and move_media / bulk_move_media relocate individual items.