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/duplicate
POST /api/v1/projects/:id/transfer
GET /api/v1/project-transfers
POST /api/v1/project-transfers/:id/accept
POST /api/v1/project-transfers/:id/decline
DELETE /api/v1/project-transfers/:id
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
POST /api/v1/sessions/move
GET /api/v1/sessions/:sessionId/generations
POST /api/v1/sessions/:sessionId/generations/move
POST /api/v1/sessions/:sessionId/split
POST /api/v1/sessions/organize/undo/:operationIdDeletion 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | No | Case-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. |
page | number | No | 1-indexed page number. Default 1. |
limit | number | No | Results per page, 1–200. Default 50. |
include_archived | string | No | The 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, "thumbnail_url": null },
{ "id": "65f1c8a2e4b0a3c1d9f5e456", "name": "Shared Brand Kit", "description": null, "role": "edit", "is_default": false, "is_archived": false, "thumbnail_url": null },
{ "id": "65f1c8a2e4b0a3c1d9f5e789", "name": "API Generations", "description": null, "role": "owner", "is_default": true, "is_archived": false, "thumbnail_url": null }
],
"pagination": { "page": 1, "limit": 50, "total": 3, "returned": 3, "has_more": false }
}| Field | Type | Notes |
|---|---|---|
id | string | Project ObjectId — this is what you pass as project_id. |
name | string | Project name. |
description | string | null | User-written brief, whitespace-collapsed and clipped to 400 characters (ending in …) in the list. null when empty. Call GET /v1/projects/:id for the full text. |
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. |
is_archived | boolean | true when the project has been archived. Only ever true if you asked for include_archived=true. |
thumbnail_url | string | null | The project's manually chosen cover image, or null when none was set (the app's recent-media mosaic is not computed here). |
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.
| 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.
Get one project
GET /api/v1/projects/:idReturns 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). A malformed :id returns 400; a missing,
deleted, or view-only project returns 404. description is null when empty. 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| 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.
Duplicating a Project
POST /api/v1/projects/:projectId/duplicateCopies a project you own into a new project you own. No body. The copy is named "<original name> (Fork)" (with a (n) suffix if that name is already taken) and keeps the description and manual cover.
What travels: the cast (Visual DNAs, moodboards and the color palette you personally own are copied, not shared — organization-shared or global cast assets are left out of the copy), uploaded media, every tool session with its generations, Flow canvases, and AI Docs. What does not: project context sources, the derived AI profile, and chat sessions — these are listed in stats.notCopied. Generations still running in the source are skipped.
Large projects are copied up to fixed ceilings (2,000 media items, 400 tool sessions, 500 generations per session, 20,000 generations overall, 200 Flow canvases, 200 AI Docs); when a ceiling is hit, stats.truncated is true.
Guards, in order:
- Owner only. A member with any shared permission gets
403withcode: "OWNER_REQUIRED"; a non-member gets403 PROJECT_ACCESS_DENIED; an unknown or deleted project gets404. Review projects return400. - Shares the 10 requests per minute project-creation limiter with
POST /v1/projects. - Counts against your plan's project cap (
403 PROJECT_LIMIT_EXCEEDED, same as create).
This route is a passthrough to the in-app controller, so the response is the app envelope, not the SDK project shape:
{
"status": true,
"message": "Project duplicated",
"data": {
"projectId": "65f1c8a2e4b0a3c1d9f5eaaa",
"name": "Acme Campaign (Fork)",
"project": { "_id": "65f1c8a2e4b0a3c1d9f5eaaa", "name": "Acme Campaign (Fork)", "…": "full project document" },
"stats": {
"sessions": 12,
"generations": 148,
"skippedInFlight": 0,
"skippedSessions": 0,
"mediaCopied": 37,
"mediaExcluded": 0,
"truncated": false,
"dnas": 2,
"moodboards": 1,
"docs": 1,
"referencedFolderId": "65f1c8a2e4b0a3c1d9f5ebbb",
"referencedMedia": 96,
"notCopied": ["contextFiles", "projectProfile", "chat"]
}
}
}referencedFolderId / referencedMedia describe a media folder in the copy that links (does not copy) the original's generated outputs. stats may also carry referencedMediaEmpty: true (the source had no generated media to link) or referencedMediaFailed: true (the folder could not be built; the rest of the copy is intact). An unexpected failure returns 500 with { "status": false, "message": "Failed to duplicate project" }.
Transferring a Project to Another Account
Hand a project you own to another existing Kolbo account. Nothing moves until the recipient accepts, within 7 days.
POST /api/v1/projects/:id/transfer
GET /api/v1/project-transfers
POST /api/v1/project-transfers/:id/accept
POST /api/v1/project-transfers/:id/decline
DELETE /api/v1/project-transfers/:idStart a transfer (owner)
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email of the existing Kolbo account that should become the owner. Missing or blank returns 400 with no code; a malformed address returns 400 INVALID_EMAIL. |
keep_sender_as_editor | boolean | No | Default true: after the transfer you stay on the project with edit permission. Only the literal false removes your access. |
Returns 201 with { "success": true, "transfer": { … } }. Rules enforced server-side:
- You must own the project; it must not be deleted or archived (otherwise
404 PROJECT_NOT_FOUND). - Projects that belong to a team workspace or organization cannot be transferred (
400 ORG_PROJECT_NOT_TRANSFERABLE). - One pending transfer per project (
409 TRANSFER_ALREADY_PENDING— cancel it first). - At most 10 transfer requests per sender per 24 hours, and 3 per sender/recipient pair (
429 TRANSFER_RATE_LIMITED).
List transfers
| Parameter | Type | Required | Description |
|---|---|---|---|
direction | string | No | incoming (default — transfers offered to you) or outgoing (ones you sent). Any other value is treated as incoming. |
status | string | No | pending (default), accepted, declined, cancelled, expired, or failed. Any other value falls back to pending. |
Returns { "success": true, "transfers": [ … ] }, newest first, at most 50 rows, no pagination.
Accept, decline, cancel
accept and decline are for the recipient; DELETE cancels a pending transfer you sent. All three take no body and return { "success": true, "transfer": { … } }. On accept, transfer also carries moved: a map of record kind (collection name) to the number of records moved to you.
On accept, every record in the project that the sender owns moves to the recipient with the same ids (sessions, generations, media, docs, and so on). The sender's Visual DNAs, moodboards and palette in the project cast are copied to the recipient and the project points at the copies; the sender keeps the originals. Custom voices stay with the sender. The credit ledger keeps the original payer, and future billing on the project switches to the recipient. Accept is refused with 409 TRANSFER_BLOCKED_IN_FLIGHT (the body also carries running, the count) while generations are still running in the project — the transfer stays pending, so retry later.
Transfer object
{
"id": "6702a0c1e4b0a3c1d9f5e001",
"project_id": "65f1c8a2e4b0a3c1d9f5e123",
"project_name": "Acme Campaign",
"from_user": { "id": "64aa…", "name": "Dana Levi", "email": "[email protected]" },
"to_user": { "id": "64bb…", "name": "Sam Cohen", "email": "[email protected]" },
"to_email": "[email protected]",
"keep_sender_as_editor": true,
"status": "pending",
"expires_at": "2026-10-12T09:00:00.000Z",
"created_at": "2026-10-05T09:00:00.000Z",
"responded_at": null
}from_user / to_user carry name and email on list results; on the create, accept, decline, and cancel responses they carry only id. A pending transfer past its expiry is reported as expired.
Transfer errors use the SDK envelope plus a code: { "success": false, "code": "…", "error": "…" }. Besides the codes above: 400 INVALID_PROJECT / INVALID_EMAIL / SELF_TRANSFER, 404 RECIPIENT_NOT_FOUND (no account uses that email), 404 TRANSFER_NOT_FOUND (no pending transfer you can act on), 410 TRANSFER_EXPIRED (accept after 7 days), 409 TRANSFER_STATE_CHANGED (the project was deleted, archived, or changed owner before accept), and 500 TRANSFER_FAILED (accept failed and nothing was changed; the transfer stays pending).
All writes here — create, accept, decline, cancel — need a write-enabled API key (a read-only key gets
403 API_KEY_READ_ONLY) and share the 10 requests per minute project-creation limiter. GET /project-transfers
has no per-endpoint limiter.
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. |
Movable session types: image, image_edit, video, video_from_image, elements, first_last_frame, lipsync, video_from_video, global_video_edit, music, speech, sound, and chat — i.e. the session_id returned on those generation submits, plus chat conversations. creative_director, transcription, and global_image_edit sessions cannot be moved through the API and return 400 with code: "SESSION_TYPE_NOT_MOVABLE" (Creative Director owns nested child sessions; move it in the app). 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. A session with a generation still running returns 409 GENERATIONS_IN_FLIGHT; wait for it to finish.
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,
"operation_id": "65f1a00"
},
"operation_id": "65f1a00"
}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.
operation_id (returned both inside session and at the top level) reverses the move — see Undoing a Reorganization.
Other failures from the move itself use the SDK envelope with a code: 403 PROJECT_EDIT_REQUIRED (you are below edit on the session's current project), 404 PROJECT_NOT_FOUND, and 409 SESSION_STATE_CHANGED (the session or its media changed mid-move — reload and retry). A target project_id you cannot write to returns the SDK_PROJECT_* codes in Errors.
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| Field | Type | Required | Description |
|---|---|---|---|
session_ids | string[] | Yes | 1–100 session ObjectIds. Mixed types are fine. Also accepted as sessionIds. More than 100 movable sessions returns 400 BATCH_TOO_LARGE. |
project_id | string | Yes | Target project ObjectId; you need edit+ on it. Also accepted as newProjectId. |
type | string | No | Session type hint (same 17 keys as above), only useful when every id in the batch is the same type — when set, ids of any other type are reported as SESSION_NOT_FOUND. 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 — INVALID_ID, SESSION_NOT_FOUND (missing or no access), SESSION_TYPE_NOT_MOVABLE, GENERATIONS_IN_FLIGHT, or a permission code. Always surface failed to the user: a 200 here does not mean everything moved. When none of the ids can be resolved, the route returns 400 with "error": "None of the given sessions can be moved." and the same failed array.
operation_ids carries one id per moved session; see Undoing a Reorganization. This route shares the 20-per-minute bucket with the single-session move.
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.
Moving and splitting generations works for image sessions (image, image_edit), image-to-video sessions (video, video_from_image, elements, first_last_frame), lipsync, and video-to-video sessions (video_from_video, global_video_edit). On music, speech, sound and chat sessions the move and split routes return 400 UNSUPPORTED_TOOL — move those whole with the routes above; the listing still answers for them, but with empty prompt / output_urls, thumbnail_url: null and media_count: 0 (chat sessions list no generations). creative_director, transcription and global_image_edit sessions return 400 SESSION_TYPE_NOT_MOVABLE on all three routes.
The listing is limited to 120 requests per minute; the move and split routes share the 20-per-minute bucket with the session moves above.
Listing a session's generations
GET /api/v1/sessions/:sessionId/generationsReturns 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
}
]
}Optional query parameter: type — the same session-type hint as above; an unknown value returns 400.
in_flight: true means the generation is still running and cannot be moved yet. prompt is clipped to 400 characters, and output_urls lists at most the first 5 outputs — output_count is the true total. can_edit tells you whether you can move anything out of this session.
Moving generations into an existing session
POST /api/v1/sessions/:sessionId/generations/move| Field | Type | Required | Description |
|---|---|---|---|
generation_ids | string[] | Yes | Non-empty. From the listing above. Whole entries only. Also accepted as generationIds. |
target_session_id | string | Yes | Destination session, same session kind, different from the source (400 SAME_SESSION). May live in another project you can edit. Also accepted as targetSessionId. |
type | string | No | Session 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.
{
"success": true,
"operation_id": "65f1a05",
"moved_generations_count": 3,
"moved_media_count": 6,
"target_session_id": "65f1888",
"project_id": "65f1123"
}An id that is not in the source session returns 404 GENERATIONS_NOT_IN_SESSION; a running one returns 409 GENERATIONS_IN_FLIGHT.
Splitting a session
POST /api/v1/sessions/:sessionId/split| Field | Type | Required | Description |
|---|---|---|---|
generation_ids | string[] | Yes | Non-empty. Generations to carve out. Also accepted as generationIds. |
name | string | Yes | Name for the new session. Trimmed; must not be empty. |
project_id | string | No | Put the new session in a different project (you need edit on both). Defaults to the source session's project. Also accepted as targetProjectId. |
type | string | No | Session 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
The single-session move (PATCH /v1/sessions/:sessionId/project), batch move (POST /v1/sessions/move), generation move, and split each return an operation_id, reversible for 15 minutes by the user who made it.
POST /api/v1/sessions/organize/undo/:operationIdPermissions 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 }kind is move_session, move_generations, or split. Other outcomes: 404 OPERATION_NOT_FOUND (unknown id, or not yours), 409 ALREADY_UNDONE, 410 UNDO_WINDOW_CLOSED (past 15 minutes), and 400 for a malformed id.
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/:assetIdGET requires any project access (including view). Link / note / unlink require edit.
All four routes are limited to 120 requests per minute. Project-permission failures on
these routes come from the shared project middleware (see Errors).
List
Returns { "success": true, "visual_dnas": [ … ], "moodboards": [ … ] } — a flat
visual_dnas[] (id, name, dna_type, thumbnail_url, description, note) and
moodboards[] (id, name, thumbnail_url, summary, note). dna_type is the DNA's own
type (character, environment, product, style, scene). summary is the
moodboard's master prompt. description, summary and note are "" when empty.
Only active DNAs are listed; DNAs and moodboards originally created inside the project
are included even if never explicitly linked.
Link
{ "asset_type": "visual_dna", "asset_id": "6601a1b2c3d4e5f6a7b8c9d0" }asset_type is visual_dna or moodboard. Also accepts camelCase
assetType / assetId. asset_id must be a 24-character hex ObjectId. You may link
any asset you can access — your own, your organization's, or one from a project shared
with you. Linking is idempotent. Responds
{ "success": true, "asset": { "id", "name", "asset_type" } }; an unknown or
inaccessible asset returns 404, a missing/invalid type or id returns 400.
Purpose note
PUT …/note with { "note": "hero, use on dark backgrounds" }. note is required
(omitting it returns 400); it is trimmed and silently truncated to 1,000
characters. An empty string clears the note. Responds
{ "success": true, "asset_id": "…", "note": "…" } with the stored value. 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).
Unlink
Removes the tag only and responds { "success": true, "asset_id": "…", "asset_type": "…" }.
Unlinking an asset that was not linked still succeeds. 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| 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 / generation listing / generation move / split) the session does not exist, is deleted, or you have no access to its project. |
| 400 | SESSION_TYPE_NOT_MOVABLE / UNSUPPORTED_TOOL | (Session organization) the session type cannot be moved or split over the API — see the type lists above. |
| 409 | GENERATIONS_IN_FLIGHT / SESSION_STATE_CHANGED / UNDO_STATE_CHANGED | (Session organization) a generation is still running, or the session changed mid-operation. Retry after it settles. |
| 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. |
| 400 | INVALID_PROJECT_ID | (Routes with :projectId — context, profile, assets, duplicate) the path id is not a valid ObjectId. |
| 404 | PROJECT_NOT_FOUND / PROJECT_DELETED | (Routes with :projectId) the project does not exist or was deleted. |
| 403 | PROJECT_ACCESS_DENIED | (Routes with :projectId) you are not a member of the project. Unlike GET /v1/projects/:id, these routes answer 403, not 404. |
| 403 | INSUFFICIENT_PERMISSION / OWNER_REQUIRED | (Routes with :projectId) you are a member but below the required tier (edit for writes, owner for duplicate). |
The :projectId middleware errors use the shape { "success": false, "message": "…", "code": "…" } — the message is under message, not error.
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, duplicate_project, transfer_project, list_project_transfers, respond_project_transfer (one tool for accept / decline / cancel), move_session, bulk_move_sessions, list_session_generations, move_generations_to_session, split_session, undo_session_organization, 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, create_project and duplicate_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
Media Library
Upload, list, and filter all the media in a user's Kolbo library — by project, folder, type, and section — the same view as the desktop app and Adobe plugin.
Agents & Sessions
Manage reusable custom chat agents (Skills), and enumerate, rename, and trash sessions across every generation type.