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/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
PATCH /api/v1/sessions/:sessionId/projectDeletion 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).
This endpoint takes no query parameters.
curl https://api.kolbo.ai/api/v1/projects \
-H "X-API-Key: YOUR_API_KEY"Response:
{
"success": true,
"projects": [
{ "id": "65f1c8a2e4b0a3c1d9f5e123", "name": "Acme Campaign", "role": "owner", "is_default": false },
{ "id": "65f1c8a2e4b0a3c1d9f5e456", "name": "Shared Brand Kit", "role": "edit", "is_default": false },
{ "id": "65f1c8a2e4b0a3c1d9f5e789", "name": "API Generations", "role": "owner", "is_default": true }
]
}| Field | Type | Notes |
|---|---|---|
id | string | Project ObjectId — this is what you pass as project_id. |
name | string | Project name. |
role | string | owner | full | edit. |
is_default | boolean | true only for the project literally named API Generations that you own — the bucket SDK calls fall into when project_id is omitted. |
The query is sorted by last-updated first and capped at 200 projects — there is no pagination.
The cap is applied before view-only shares are filtered out, so an account with many view-only
shares can get back noticeably fewer than 200 writable projects. Archived projects are still
included, and so are auto-created review projects; use GET /project/lightweight if you need
to tell archived from active.
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | No | Case-insensitive name filter. Regex-escaped server-side, so user text is safe to pass through. |
sortBy | string | No | date | created_at | updated_at | name. date and created_at both sort on the creation timestamp. Omitted → unsorted. |
sortDirection | string | No | desc for descending; any other value (including omitted) sorts ascending. |
includeArchived | string | No | The 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| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Project name. Must be a non-empty string; trimmed server-side. No length cap on this route. |
description | string | No | Project 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.
Updating, Archiving, Unarchiving
PUT /api/v1/projects/:id| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New name. |
description | string | No | New 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/unarchiveBoth take an empty body and require full permission or ownership — edit 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| Field | Type | Required | Description |
|---|---|---|---|
project_id | string | Yes | Target project ObjectId. You need edit / full / owner permission on it. Also accepted as newProjectId. |
type | string | No | Session 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 you own: generation sessions (the session_id returned on every generation submit), chat conversations, transcription sessions, and so on. Ownership of the session is required — being a member of the session's project is not enough.
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_media_count is the number of media library items whose project actually changed (matched on session_id + your user id — items already sitting in the target project are not counted). 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 moved_media_count: 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.
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| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Public web page to fetch and analyze. Trimmed server-side, and validated against internal/private addresses — a blocked target returns 400. |
title | string | No | Display 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| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Raw text (script, notes, brand facts). Also accepted as content. Trimmed, then stored for retrieval — only the first 60,000 characters are kept and indexed. |
title | string | No | Display 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/:fileKeyGET 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/regenerateThe 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
| Status | Code | Cause |
|---|---|---|
| 400 | SDK_PROJECT_INVALID_ID | project_id is not a valid ObjectId. |
| 403 | SDK_PROJECT_ACCESS_DENIED | You have view permission on the project but not edit or higher. |
| 404 | SDK_PROJECT_NOT_FOUND | The 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.) |
| 403 | PROJECT_LIMIT_EXCEEDED | Your 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, create_project, update_project, archive_project, unarchive_project, move_session, 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/lightweight — list_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.
Related
Agents & Sessions
Enumerate sessions across every generation type; manage custom chat agents
AI Docs
Project-scoped documents you author through the API
Media Library
List and move individual media items between projects
Color Palettes
Palettes are activated per project — find the id here first
Errors & Limits
Rate limits and the shared error envelope