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.
The Media Library API exposes the same library the user sees in the Kolbo web app, desktop app, and Adobe plugin. Every uploaded file and every AI-generated output the user has saved is here, with rich filtering by project, folder, type, and section (category).
The base endpoint, GET /api/v1/media, was originally a thin list of uploads. It has been extended with the full filter set used by the desktop app — the older parameter spellings (pageSize, searchTerm) continue to work exactly as before.
Endpoints
| Endpoint | Description |
|---|---|
POST /v1/media/upload | Upload a file (multipart) and receive a stable Kolbo CDN URL |
POST /v1/media/upload-ticket | Mint a short-lived, upload-only ticket so a browser can upload without your API key |
GET /v1/media | List media with filters by project, folder, type, section, source, search, and sort |
GET /v1/media/:id | Fetch one media item by id (full metadata) |
DELETE /v1/media/:id | Soft-delete (move to trash, 30-day recovery) — owner only |
POST /v1/media/:id/restore | Restore a trashed item — owner only |
DELETE /v1/media/:id/permanent | Permanently delete (S3 + DB) — owner only, NOT reversible |
PATCH /v1/media/:id/project | Re-assign item to a different project |
POST /v1/media/bulk/delete | Bulk soft-delete (≤1000 ids) |
POST /v1/media/bulk/restore | Bulk restore from trash (≤1000 ids) |
POST /v1/media/bulk/permanent | Bulk permanent delete (≤1000 ids) — NOT reversible |
POST /v1/media/bulk/move | Bulk move to project (≤1000 ids, atomic) |
GET /v1/media/stats | Item counts + total storage bytes (optionally per project) |
POST /v1/media/:id/favorite | Mark a media item as favorited (idempotent) |
DELETE /v1/media/:id/favorite | Remove a media item from favorites (idempotent) |
GET /v1/media/folders | List the user's media folders (owned + shared + project-assigned) |
POST /v1/media/folders | Create a new folder |
PUT /v1/media/folders/:id | Update folder name / description / color / icon (owner only) |
DELETE /v1/media/folders/:id | Soft-delete a folder (owner only) |
POST /v1/media/folders/:id/items | Add media items to a folder |
DELETE /v1/media/folders/:id/items | Remove media items from a folder |
POST /v1/media/folders/:id/share | Share a folder with users by email (owner only) |
DELETE /v1/media/folders/:id/share/:user_id | Revoke a user's access (owner only) |
POST /v1/media/folders/:id/move-contents | Move every item in a folder to a project you own |
Rate limits are per authenticated user, per minute, and are enforced separately per group:
POST /v1/media/upload— 10/min, sharing one bucket with the general SDK generation routes (/v1/generate/video,/music,/speech, and friends)./v1/generate/imageand/v1/transcribehave their own separate buckets, so uploads do not compete with those.POST /v1/media/bulk/*andPOST /v1/media/folders/:id/move-contents— 20/min.- Every other media route (list, get, stats, favorites, folder CRUD, upload-ticket) — 120/min.
Exceeding a limit returns 429 with retryAfter: 60.
Finding a Generation's Output Again
The media library is the recovery path when a poll loop dies, a URL is lost, or you need an asset long after the generation finished. Two routes cover it:
| You have | Call |
|---|---|
The generation_id from a /v1/generate/* submit | GET /v1/media/:id — it falls back to a generation id lookup when no media item matches directly, and returns that generation's lowest-index media item |
| Nothing but a rough idea of what you made | GET /v1/media?category=ai&sort=created_desc (add project_id, type, or search) |
Output URLs are opaque strings — do not hardcode the host. More than one CDN host appears across these docs (media.kolbo.ai and cdn.kolbo.ai). Never allow-list, pattern-match, or rebuild a URL: read the exact string from result.urls[] (or media[].url) and use it verbatim.
They are plain links — no query string, no signature, no embedded credentials. Fetch them with an ordinary GET and do not send X-API-Key to the CDN host; that header is for api.kolbo.ai.
No lifetime is published for them. Nothing here guarantees an asset stays reachable at the same address indefinitely, and DELETE /v1/media/:id/permanent removes the underlying file outright. If an asset matters, download the bytes into your own storage rather than treating a Kolbo URL as your system of record.
Two ids are easy to confuse. POST /v1/media/upload returns media.id — the stored-file id, which GET /v1/media/:id and the folder / bulk routes reject with 404. The MediaLibraryItem id those routes want is the id from GET /v1/media. The stable handle to keep from an upload is media.url; match on it to find the library id. See the callout on Upload Media.
List Media
The primary discovery endpoint. Returns the user's media — uploaded files and AI-generated outputs they have saved — filtered by any combination of project, folder, type, section, source, and free-text search.
Endpoint
GET /api/v1/mediaQuery Parameters
All parameters are optional. The endpoint defaults to the user's full library, newest first, 50 items per page.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string (ObjectId) | No | Restrict to a single project. The caller must have access (owner, co-owner, or shared user); without access the response is an empty list, not an error. A malformed id returns 400. |
folder_id | string (ObjectId) | No | Restrict to a folder. Takes precedence over project_id. Requires the caller to own the folder or be in its shared_with list — otherwise 404. |
type | string | No | Media type. One of image, video, audio, all (default all). Any other value returns 400. The library can also hold 3d_model and document items (stock 3D imports, and documents uploaded through the upload-ticket flow) — no type value selects those, so they only surface under the default all. |
category | string | No | "Section" filter — matches the desktop app sidebar. One of ai, uploaded, edited, favorites, training-lab, all (default all). Any other value returns 400. |
source_type | string | No | Lower-level provenance. One of uploaded, generated, chat-generated. Any other value returns 400. Note generated matches both generated and chat-generated items; pass chat-generated to isolate chat outputs. Use category for the common case. |
source | string | No | Free-form metadata.source tag (e.g. flow for Flow-generated items). Truncated to 64 characters. |
preset_id | string (ObjectId) | No | Items generated using a specific preset. |
moodboard_id | string (ObjectId) | No | Items generated using a specific moodboard. |
search | string | No | Case-insensitive substring match across filename and the generation metadata: prompt, presetName, moodboardName, visualDnaNames, musicTitle, musicStyle, lyrics, voiceName, modelName, model. Regex-escaped server-side, so user-typed text is safe to pass through. |
sort | string | No | Order. One of created_desc (default), created_asc, name_asc, name_desc. Any other value returns 400. Ignored when folder_id is set. |
page | integer | No | 1-indexed page. Default 1. Must be a positive integer, else 400. |
page_size | integer | No | Items per page. Default 50, silently capped at 200. Must be a positive integer, else 400. |
include_deleted | string | No | Pass the literal string "true" to list only soft-deleted (trashed) items. Default false. |
Folder listings behave differently. When folder_id is set the folder's contents are returned as-is: items come back in folder order (manual position, then most-recently-added first) and every other filter — sort, type, category, source_type, source, search, preset_id, moodboard_id, project_id — is ignored. Only page, page_size, and include_deleted still apply.
Section / Category Reference
The category parameter mirrors the section buttons in the Kolbo desktop app and Adobe plugin:
category | Meaning |
|---|---|
ai | Anything AI-generated (text-to-image, video, music, speech, sound, lipsync, etc.) |
uploaded | Files the user uploaded themselves (no AI generation) |
edited | AI-edited variants — matches only items whose metadata.category is exactly edited-image |
favorites | Items starred by anyone in the project — favorites are project-level, not per-user |
training-lab | Assets created or used inside Training Lab |
all | No category filter (default) |
Stock-library imports are stored with sourceType: "stock" and metadata.category: "stock-footage". Neither value is accepted by category or source_type, so imported stock assets are only returned when you leave both filters off.
Examples
List the user's most recent 20 items across everything:
curl "https://api.kolbo.ai/api/v1/media?page_size=20" \
-H "X-API-Key: kolbo_live_..."List all videos in a specific project:
curl "https://api.kolbo.ai/api/v1/media?project_id=6601a1b2c3d4e5f6a7b8c9d0&type=video" \
-H "X-API-Key: kolbo_live_..."List only AI-generated images the user has favorited:
curl "https://api.kolbo.ai/api/v1/media?type=image&category=favorites" \
-H "X-API-Key: kolbo_live_..."List everything in a folder:
curl "https://api.kolbo.ai/api/v1/media?folder_id=6601a1b2c3d4e5f6a7b8c9d0" \
-H "X-API-Key: kolbo_live_..."Free-text search across filenames and prompts inside one project:
curl "https://api.kolbo.ai/api/v1/media?project_id=6601...&search=sunset" \
-H "X-API-Key: kolbo_live_..."List the trash:
curl "https://api.kolbo.ai/api/v1/media?include_deleted=true" \
-H "X-API-Key: kolbo_live_..."Response
{
"success": true,
"media": [
{
"id": "660aa1b2c3d4e5f6a7b8c9d0",
"url": "https://cdn.kolbo.ai/u/123/abc.mp4",
"thumbnail_url": "https://cdn.kolbo.ai/u/123/abc-thumb.jpg",
"name": "sunset-beach.mp4",
"filename": "sunset-beach.mp4",
"type": "video",
"media_type": "video",
"category": "text-to-video",
"size": 4823901,
"source_type": "generated",
"generation_type": "textToVideoGeneration",
"session_type": "text_to_video",
"prompt": "Cinematic sunset over an empty beach, slow dolly-in",
"width": 1920,
"height": 1080,
"duration": 8,
"is_favorited": true,
"nsfw_detected": false,
"project_id": "6601a1b2c3d4e5f6a7b8c9d0",
"session_id": "6601...",
"user_id": "65f0...",
"user_email": "you@example.com",
"user_name": "You",
"created_at": "2026-05-10T18:24:31.000Z",
"metadata": { "...": "..." }
}
],
"pagination": {
"page": 1,
"page_size": 50,
"total_items": 312,
"total_pages": 7,
"has_next": true
}
}Notes on the response:
categoryis the resolved bucket for the item:text-to-video,edited-image,generated,uploaded, etc. It maps to (but is not identical to) thecategoryquery filter — use the query filter for selection and the responsecategoryfor display.generation_typeis the internal generation model name (textToVideoGeneration,imageGeneration,musicGeneration, …) and isnullfor uploads.session_typeis the snake_case tool name derived fromcategory(text-to-video→text_to_video,edited-image→image_editing,music→music, …); an unrecognised category is passed through unchanged, and the catch-all categorygeneratedmaps totext_to_imageregardless of the actual media type — so treatsession_typeas a display hint rather than a stable enum.is_favoritedis project-level, not per-user: it istruewhen any member has favorited the item (see Favorite a Media Item).user_emailanduser_nameare only resolved when you passproject_id. Without a project filter the owner lookup is skipped entirely and both come backnull— even for your own items.user_idis always present.total_itemsis only computed on page 1. On page 2 and beyond the server returnstotal_items: -1andtotal_pages: 0— that is not an error, and it applies to every listing (project, folder, or unfiltered). Paginate onhas_next, and keep the count you got from page 1.prompt,width,height,durationare best-effort and may benullfor older items or uploads.metadatais always returned, and carries the generation's own metadata pluscreditsUsed,generationDurationSeconds,projectId, andsessionId.
Items flagged as NSFW come back with a placeholder URL. When nsfw_detected is true, both url and thumbnail_url in this list response are replaced with a static placeholder image served from the API host (/assets/nsfw-placeholder.webp) — the real CDN URL is not returned. The substitution is skipped only for accounts whose resolved NSFW behaviour is unrestricted: either an admin-set unrestricted mode, or an account that has explicitly opted into NSFW content in its own settings. GET /v1/media/:id does not apply this substitution and returns the real URL.
Validation Errors
Invalid enum values return 400:
curl "https://api.kolbo.ai/api/v1/media?type=gif" -H "X-API-Key: ..."
# { "success": false, "error": "Invalid type \"gif\". Allowed: all, image, video, audio." }Get One Media Item
GET /api/v1/media/:idReturns one item with its full metadata — reference media, Visual DNA names and thumbnails, the Color DNA palette snapshot, the quality tier, credits used, and generation duration.
Access: the caller must own the item, or have access to the item's project, or be a system admin, or have an active organization that the item's owner also belongs to — otherwise 403. That last rule is plain org membership, not a role check: any member whose active organization contains the owner can read the item. (activeOrganizationId is populated for API-key callers too, falling back to the first entry in the account's organization list.) Trashed items are treated as not found (404). An id that is not a valid ObjectId returns 400.
If no media item matches :id directly, the server falls back to looking the id up as a generation id and returns that generation's lowest-index media item — useful when you only kept the id returned by a /v1/generate/* call.
curl "https://api.kolbo.ai/api/v1/media/660aa1b2c3d4e5f6a7b8c9d0" \
-H "X-API-Key: kolbo_live_..."{
"success": true,
"media": {
"id": "660aa1b2c3d4e5f6a7b8c9d0",
"type": "video",
"category": "text-to-video",
"filename": "sunset-beach.mp4",
"url": "https://cdn.kolbo.ai/u/123/abc.mp4",
"size": 4823901,
"created": "2026-05-10T18:24:31.000Z",
"projectId": "6601a1b2c3d4e5f6a7b8c9d0",
"creditsUsed": 120,
"generationDurationSeconds": 43,
"sessionId": "6601...",
"session_id": "6601...",
"userId": "65f0...",
"userEmail": "you@example.com",
"userName": "You",
"userAvatar": null,
"isFavorited": true,
"isDisliked": false,
"generationType": "textToVideoGeneration",
"sessionType": "text_to_video",
"metadata": { "...": "..." },
"referenceMedia": []
}
}This endpoint does not use the snake_case list shape. It returns the internal camelCase document: created (not created_at), projectId, userId, isFavorited, generationType, sessionType. There is no thumbnail_url, name, media_type, source_type, prompt, width, height, duration, or nsfw_detected field here — read the thumbnail from metadata.thumbnailUrl and the dimensions/prompt from metadata. If you need the list shape, use GET /v1/media with a filter instead.
category here resolves to metadata.category and falls back to sourceType (so an uploaded file reports uploaded); isDisliked mirrors the "hide" state on the source generation and is absent for generation types that do not support it. Reading an item also lazily backfills referenceMedia, metadata.visualDnaNames, metadata.colorPalette, and metadata.quality on older records.
Delete / Restore / Permanently Delete
DELETE /api/v1/media/:id # soft-delete (30-day trash, reversible)
POST /api/v1/media/:id/restore # bring back from trash
DELETE /api/v1/media/:id/permanent # hard-delete (removes from S3 + DB)All three are owner-only — project access is not enough. An item you don't own returns 404.
Soft-delete is the standard "delete" — items move to trash and can be restored for 30 days.
Restore only works on items currently in the trash; a live (or unknown) item returns 404.
Permanent delete is not reversible — it removes the MongoDB record, the S3 file, every folder membership, and the source generation record. It is idempotent: deleting something already gone still returns 200.
To list trashed items, use GET /v1/media?include_deleted=true.
# Soft-delete
curl -X DELETE "https://api.kolbo.ai/api/v1/media/660..." \
-H "X-API-Key: kolbo_live_..."
# → { "success": true, "id": "660...", "message": "Media moved to trash (30-day recovery)" }
# Restore
curl -X POST "https://api.kolbo.ai/api/v1/media/660.../restore" \
-H "X-API-Key: kolbo_live_..."
# Hard-delete (NOT reversible)
curl -X DELETE "https://api.kolbo.ai/api/v1/media/660.../permanent" \
-H "X-API-Key: kolbo_live_..."Move a Media Item to Another Project
PATCH /api/v1/media/:id/project| Body field | Type | Required | Description |
|---|---|---|---|
project_id | string (ObjectId) | Yes | Target project. The caller must have access to it (owner, co-owner, or shared user). Missing → 400; malformed → 400; no access → 403. Legacy alias: newProjectId. |
The caller must own the item and it must not be in the trash, otherwise 404.
curl -X PATCH "https://api.kolbo.ai/api/v1/media/660.../project" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "project_id": "6601a1b2c3d4e5f6a7b8c9d0" }'
# → { "success": true, "id": "660...", "project_id": "6601a1b2c3d4e5f6a7b8c9d0" }Bulk Operations
POST /api/v1/media/bulk/delete { "media_ids": [...] }
POST /api/v1/media/bulk/restore { "media_ids": [...] }
POST /api/v1/media/bulk/permanent { "media_ids": [...] } # NOT reversible
POST /api/v1/media/bulk/move { "media_ids": [...], "project_id": "…" }| Body field | Type | Required | Description |
|---|---|---|---|
media_ids | string[] (ObjectIds) | Yes | Non-empty, max 1000 per call. Every entry must be a valid ObjectId — one bad id rejects the whole call with 400 Invalid media id: …. Legacy alias: mediaIds. |
project_id | string (ObjectId) | bulk/move only | Target project. Missing → 400; malformed → 400; caller has no access (owner, co-owner, or shared user) → 403. Legacy alias: newProjectId. |
Ownership rules and response shapes differ per endpoint:
| Endpoint | Ownership | Response fields |
|---|---|---|
bulk/delete | Ids not owned by the caller are skipped | deleted_count (number), errors — an array holding at most one entry, { "error": "N item(s) not found or not owned by user" }. It is [] when every id matched — and also [] in the edge case where none matched, so check deleted_count, not errors.length |
bulk/restore | Only trashed items owned by the caller are restored | restored_count (number), not_found — a count, not a list: supplied ids minus restored ids |
bulk/permanent | Ids not owned by the caller are skipped | deleted_count (number), errors — same single-entry shape as bulk/delete |
bulk/move | Atomic — every id must be owned by the caller and not in the trash, else 403 ("You own N of M items") and nothing moves | moved_count (number), project_id |
curl -X POST "https://api.kolbo.ai/api/v1/media/bulk/delete" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "media_ids": ["660...a", "660...b", "660...c"] }'
# → { "success": true, "deleted_count": 3, "errors": [] }Storage Stats
GET /api/v1/media/stats[?project_id=…]| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string (ObjectId) | No | Scope stats to one project — counts all members' media in that project. Omit for the caller's own media across all projects. Malformed id → 400. No access to the project → all-zero stats with 200. Legacy alias: projectId. |
curl "https://api.kolbo.ai/api/v1/media/stats" \
-H "X-API-Key: kolbo_live_..."{
"success": true,
"stats": {
"total": 312,
"images": 178,
"videos": 102,
"audio": 32,
"total_size_bytes": 4823901234
}
}Trashed items are excluded from the counts.
total counts every media type, including the 3d_model and document items that have no bucket of their own — so total can be larger than images + videos + audio. total_size_bytes sums the stored size of every counted item (items with no recorded size contribute 0).
Move Folder Contents to a Project
POST /api/v1/media/folders/:id/move-contentsMoves every item inside the folder to a different project in one call.
| Body field | Type | Required | Description |
|---|---|---|---|
project_id | string (ObjectId) | Yes | Target project. Legacy alias: newProjectId. |
Access rules — all three must hold:
- The caller owns the folder or is in its
shared_withlist, else404. - The caller owns the target project outright. Shared or
editaccess is not enough here (unlikePATCH /v1/media/:id/project), else404. - The caller owns every item in the folder, else
403with a count of the items they don't own.
An empty folder returns 200 with moved_count: 0.
curl -X POST "https://api.kolbo.ai/api/v1/media/folders/6611.../move-contents" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "project_id": "6601..." }'
# → { "success": true, "moved_count": 42, "project_id": "6601..." }Favorite a Media Item
POST /api/v1/media/:id/favorite
DELETE /api/v1/media/:id/favoriteAccess: the caller must own the item or be a member of the item's project. Both calls are idempotent.
Favorites are project-level, not per-user. Favoriting adds the caller to the item's favoritedBy list, but unfavoriting clears that list for everyone — a teammate in a shared project will see the item leave Favorites too. is_favorited in the list response is true when any member has favorited the item.
Favorited items show up in GET /v1/media?category=favorites and in the Favorites section of the desktop app and Adobe plugin.
curl -X POST "https://api.kolbo.ai/api/v1/media/660aa1b2c3d4e5f6a7b8c9d0/favorite" \
-H "X-API-Key: kolbo_live_..."
curl -X DELETE "https://api.kolbo.ai/api/v1/media/660aa1b2c3d4e5f6a7b8c9d0/favorite" \
-H "X-API-Key: kolbo_live_..."{ "success": true, "id": "660aa1b2c3d4e5f6a7b8c9d0", "is_favorited": true }List Media Folders
Folders are user-scoped — they live on the user, not on projects, and can group media across multiple projects. The structure is flat: there are no nested folders.
Endpoint
GET /api/v1/media/foldersReturns folders the caller owns, folders shared with them, and folders assigned to any project they can access, newest first. No parameters; capped at 500 folders (no pagination).
Example
curl "https://api.kolbo.ai/api/v1/media/folders" \
-H "X-API-Key: kolbo_live_..."Response
{
"success": true,
"folders": [
{
"id": "6611...",
"name": "Campaign A — hero shots",
"description": null,
"color": "#3B82F6",
"icon": "folder",
"item_count": 42,
"is_owner": true,
"shared_with_count": 0,
"shared_with": [],
"project_id": null,
"created_at": "2026-04-01T10:00:00.000Z",
"last_updated": "2026-05-12T08:14:00.000Z"
}
],
"count": 1
}Pass the id of any folder back to GET /v1/media?folder_id=... to list its contents.
A folder that appears here only because it is assigned to a shared project (project_id set, is_owner: false, and you are not in shared_with) is visible in this list but not readable or writable through the folder endpoints — listing its contents returns 404, and adding/removing items returns 404. Only direct ownership or a direct share grants access.
Create a Folder
POST /api/v1/media/folders| Body field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Trimmed; must be non-empty and ≤100 characters. |
description | string | No | ≤500 characters. |
color | string | No | Hex #RRGGBB (exactly 6 hex digits). Anything else → 400. Default #3B82F6. |
icon | string | No | Lucide icon name (e.g. folder, star, image). ≤50 characters. Default folder. The limit is enforced by the model on this route, so an over-long icon returns the platform envelope 400 { "status": false, "message": "Validation Error", "code": "VALIDATION_ERROR" } rather than the SDK's { "success": false, "error": … }. |
curl -X POST "https://api.kolbo.ai/api/v1/media/folders" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "Campaign A — hero shots", "color": "#F59E0B", "icon": "star" }'Returns 201 Created with { "success": true, "folder": { … } }.
The folder is always created against the caller alone: there is no project_id field on this route (or on the update route), so folder.project_id is null for every folder created through the API. Project-assigned folders only exist when created inside the Kolbo app.
Update a Folder
PUT /api/v1/media/folders/:idOwner only — a folder shared with you returns 404. Any subset of name / description / color / icon may be provided; omitted fields are unchanged.
| Body field | Type | Required | Description |
|---|---|---|---|
name | string | No | Trimmed; non-empty, ≤100 characters. |
description | string | null | No | ≤500 characters. Pass "" or null to clear it. |
color | string | No | Hex #RRGGBB. Sending null or a non-hex value → 400. |
icon | string | No | ≤50 characters. |
curl -X PUT "https://api.kolbo.ai/api/v1/media/folders/6611..." \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "Campaign A — final", "color": "#10B981" }'Delete a Folder
DELETE /api/v1/media/folders/:idOwner only. The folder is soft-deleted and item membership is cleared, but the items themselves remain in the user's media library. Returns { "success": true, "id": "6611..." }.
curl -X DELETE "https://api.kolbo.ai/api/v1/media/folders/6611..." \
-H "X-API-Key: kolbo_live_..."Add Items to a Folder
POST /api/v1/media/folders/:id/items| Body field | Type | Required | Description |
|---|---|---|---|
media_ids | string[] (ObjectIds) | Yes | Non-empty, max 500 per call. Every entry must be a valid ObjectId, else 400. |
Access: the caller must own the folder or be in its shared_with list, else 404.
Only media the caller owns can be added. Ids belonging to anyone else are silently dropped and returned in rejected; if none of the supplied ids are owned by the caller the call returns 403. Already-present items count towards skipped (the call is idempotent).
curl -X POST "https://api.kolbo.ai/api/v1/media/folders/6611.../items" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "media_ids": ["660...a", "660...b", "660...c"] }'{ "success": true, "folder_id": "6611...", "added": 3, "skipped": 0, "rejected": [] }Remove Items from a Folder
DELETE /api/v1/media/folders/:id/itemsBody shape matches add (media_ids, non-empty, max 500). Items remain in the user's library — only the folder membership is removed.
Unlike the add route, this one does not pre-validate id format, so a malformed id fails at the database layer and comes back in the platform error envelope — 400 { "status": false, "message": "Invalid ID format", "code": "INVALID_ID_FORMAT" } — instead of the SDK's { "success": false, "error": "Invalid media id: …" }. Valid ids that are not in the folder are simply not counted in removed.
The ids are read from a JSON request body on a DELETE, so use an HTTP client that sends one (curl -X DELETE -d, or fetch with method: "DELETE", body: …).
Access: the folder owner can remove any item. A shared member can only remove items they themselves added — ids added by someone else are simply not removed and are not counted in removed.
curl -X DELETE "https://api.kolbo.ai/api/v1/media/folders/6611.../items" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "media_ids": ["660...a"] }'{ "success": true, "folder_id": "6611...", "removed": 1 }Share a Folder
POST /api/v1/media/folders/:id/share| Body field | Type | Required | Description |
|---|---|---|---|
user_emails | string[] | Yes | Non-empty, max 50 per call. Lower-cased and trimmed server-side. Emails with no Kolbo account come back in not_found — the rest still succeed. The owner's own email is ignored. |
Owner only — a folder shared with you cannot be reshared (404).
There are no permission levels on folder shares. Sharing is a flat list of user ids: a member either has access or does not. Concretely, a shared member can list the folder's contents, add items they own, and remove items they added themselves. They cannot rename, recolor, or delete the folder, reshare it, revoke anyone's access, remove items added by the owner or other members, or move the folder's contents to another project. Sharing is also additive and idempotent — re-sharing with an existing member is a no-op.
curl -X POST "https://api.kolbo.ai/api/v1/media/folders/6611.../share" \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{ "user_emails": ["alice@example.com", "bob@example.com"] }'{
"success": true,
"folder": { "id": "6611...", "shared_with": ["65f1...", "65f2..."], "shared_with_count": 2, "...": "..." },
"shared_with_user_ids": ["65f1...", "65f2..."],
"not_found": []
}shared_with_user_ids is the set of ids resolved from this call; folder.shared_with is the folder's full member list after the update.
Revoke Folder Access
DELETE /api/v1/media/folders/:id/share/:user_idOwner only. :user_id must be a valid ObjectId (else 400) — use one of the ids in the folder's shared_with array. Idempotent: revoking a user who isn't a member still returns 200.
curl -X DELETE "https://api.kolbo.ai/api/v1/media/folders/6611.../share/65f1..." \
-H "X-API-Key: kolbo_live_..."{ "success": true, "folder": { "...": "..." }, "revoked_user_id": "65f1..." }Upload Media
POST /api/v1/media/uploadMultipart form upload. Re-hosts the file on Kolbo CDN and registers it in the user's library so you can reuse the URL in any subsequent generation call.
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | The file to upload. Max 500 MB. The media kind is routed by MIME type: video/* → video storage, audio/* → audio storage, everything else → the image pipeline. |
description | string | No | Recorded as the upload context for video and audio uploads. Ignored for images and documents. |
project_id | string (ObjectId) | No | Project to file the upload into. Requires at least edit permission — malformed → 400 SDK_PROJECT_INVALID_ID, unknown/no access → 404 SDK_PROJECT_NOT_FOUND, view-only → 403 SDK_PROJECT_ACCESS_DENIED. Omitted → the upload is not scoped to a project. |
curl -X POST https://api.kolbo.ai/api/v1/media/upload \
-H "X-API-Key: kolbo_live_..." \
-F "file=@/path/to/photo.jpg" \
-F "description=Reference photo for the campaign" \
-F "project_id=65f1c8a2e4b0a3c1d9f5e123"{
"success": true,
"media": {
"id": "660aa1b2c3d4e5f6a7b8c9d0",
"url": "https://cdn.kolbo.ai/u/123/photo.jpg",
"thumbnail_url": null,
"name": "photo.jpg",
"type": "image",
"size": 184320,
"created_at": "2026-05-10T18:24:31.000Z"
}
}Uploads are de-duplicated by file hash: re-uploading identical bytes returns the existing file and its URL instead of storing a second copy.
name and thumbnail_url are not filled in on every path: name is populated for image uploads and for dedup hits, and is null for a fresh video or audio upload; thumbnail_url is only non-null for a fresh image upload.
This route has no document path. Anything that is not video/* or audio/* — including PDF, DOCX, TXT and CSV — is pushed through the image pipeline. Image optimisation fails on those bytes, so the original file is stored under a .jpg object key (the content type is preserved) and the library item is registered with mediaType: "image", meaning it lists as type: image, never type: document. To store real documents, use the upload-ticket flow — POST /mcp/upload classifies documents and routes them to document storage.
media.id from this endpoint is the stored-file id, not the media-library id. It comes from the uploaded-file record, while GET /v1/media/:id, the folder endpoints, and every bulk operation expect a MediaLibraryItem id. Passing the upload id to those routes returns 404. The stable handle to keep is media.url — resolve the library id afterwards with GET /v1/media?category=uploaded and match on url.
This endpoint takes a multipart file only. Sending source_url in the body without a file returns 400 — download the file yourself and post the bytes, or pass the public URL straight to the generation endpoint that needs it.
Uploads are safety-screened. A file flagged by the NSFW reference check is rejected with 422 and code: "NSFW_REFERENCE_BLOCKED" after the bytes are received — including on a dedup hit, since the check runs against the resulting URL. The screen covers the video path and the image path (which, per the callout above, is also where documents land). Only audio/* uploads skip it.
Upload Ticket (browser uploads)
POST /api/v1/media/upload-ticketMints a short-lived, upload-only token so a browser or embedded UI can upload files into the user's library without ever seeing your API key. Takes no body.
curl -X POST "https://api.kolbo.ai/api/v1/media/upload-ticket" \
-H "X-API-Key: kolbo_live_..."{
"success": true,
"token": "eyJhbGciOi...",
"upload_url": "https://api.kolbo.ai/mcp/upload",
"expires_in": 900,
"max_file_mb": { "image": 50, "audio": 200, "video": 500, "document": 50 },
"accepted": "images (jpg/png/webp/gif/heic/avif), video (mp4/mov/webm/mkv), audio (mp3/wav/m4a/ogg/flac), documents (pdf/txt/md/csv/json/docx/xlsx/pptx)"
}The client then POSTs each file to upload_url as multipart:
curl -X POST "https://api.kolbo.ai/mcp/upload" \
-H "Authorization: Bearer eyJhbGciOi..." \
-F "file=@/path/to/photo.jpg" \
-F "project_id=65f1c8a2e4b0a3c1d9f5e123"Rules for the ticket flow:
- The ticket is valid for 900 seconds (15 minutes) and is bound to the user it was minted for.
- One file per request, with a hard multipart ceiling of 500 MB; the per-kind caps in
max_file_mbare then enforced after the file is classified (over the cap →413). - The declared MIME type and the filename extension must both belong to the same kind —
image/pngsent asevil.htmlis rejected. A mismatch or an unlisted type returns415. SVG and ICO are never accepted. - The
acceptedstring is a human summary; the real allowlist is slightly wider — images also takebmp/tif/tiff/heif, video also takesm4v/avi, audio also takesaac, and documents also take legacydoc/xls. - Uploads through this route are limited to 40/min per ticket holder.
- Optional form fields:
project_id(sameedit+ permission rules as/v1/media/upload) anddescription(truncated to 500 characters). - A missing, forged, or expired ticket returns
401— mint a new one. - The response is the same
{ success, media: { … } }envelope as/v1/media/upload, plusisDuplicatefor documents.
JavaScript Example
const API_KEY = "kolbo_live_YOUR_API_KEY";
const BASE = "https://api.kolbo.ai/api";
async function main() {
// 1. Discover the user's folders
const fRes = await fetch(`${BASE}/v1/media/folders`, {
headers: { "X-API-Key": API_KEY }
});
const { folders } = await fRes.json();
console.log(`User has ${folders.length} folders.`);
// 2. List AI-generated videos from a specific project, newest first
const params = new URLSearchParams({
project_id: "6601a1b2c3d4e5f6a7b8c9d0",
type: "video",
category: "ai",
sort: "created_desc",
page_size: "50"
});
const mRes = await fetch(`${BASE}/v1/media?${params}`, {
headers: { "X-API-Key": API_KEY }
});
const { media, pagination } = await mRes.json();
console.log(`Got ${media.length} of ${pagination.total_items} videos`);
for (const item of media) {
console.log(item.name, "→", item.url);
}
}
main();Python Example
import requests
API_KEY = "kolbo_live_YOUR_API_KEY"
BASE = "https://api.kolbo.ai/api"
HEADERS = {"X-API-Key": API_KEY}
# 1. List the user's favorite images across all projects
res = requests.get(
f"{BASE}/v1/media",
headers=HEADERS,
params={
"type": "image",
"category": "favorites",
"page_size": 100,
},
)
data = res.json()
for item in data["media"]:
print(item["name"], "→", item["url"])
# 2. Pagination
while data["pagination"]["has_next"]:
next_page = data["pagination"]["page"] + 1
res = requests.get(
f"{BASE}/v1/media",
headers=HEADERS,
params={
"type": "image",
"category": "favorites",
"page_size": 100,
"page": next_page,
},
)
data = res.json()
for item in data["media"]:
print(item["name"], "→", item["url"])Tips
- For shared projects, the response includes items from all members. Use
user_id/user_emailto attribute. - Combine
project_id+category=ai+type=videoto grab every AI-generated video clip from a campaign in one call — useful for batch downloads or stitching. folder_idalways wins overproject_idwhen both are set. Folders are cross-project by design.page_sizeis capped at200. For larger exports, paginate withhas_next.- The legacy query-parameter spellings (
pageSize,searchTerm,sourceType,projectId,folderId,presetId,moodboardId,includeDeleted) are still accepted alongside the snake_case names onGET /v1/media. In bodies,mediaIdsis accepted only on the fourbulk/*routes, andnewProjectIdonly onPATCH /v1/media/:id/project,POST /v1/media/bulk/move, andPOST /v1/media/folders/:id/move-contents— the folder item routes readmedia_idsand nothing else.
MCP Tools
If you are using Kolbo through the @kolbo/mcp server (Claude Desktop, Claude Code, claude.ai connector), every route on this page has a matching tool:
| Tool | Route |
|---|---|
upload_media | POST /v1/media/upload |
media_upload_widget | POST /v1/media/upload-ticket |
list_media | GET /v1/media |
get_media | GET /v1/media/:id |
get_media_stats | GET /v1/media/stats |
delete_media, restore_media, permanently_delete_media | DELETE /v1/media/:id, POST /v1/media/:id/restore, DELETE /v1/media/:id/permanent |
move_media | PATCH /v1/media/:id/project |
favorite_media, unfavorite_media | POST / DELETE /v1/media/:id/favorite |
bulk_delete_media, bulk_restore_media, bulk_permanently_delete_media, bulk_move_media | POST /v1/media/bulk/delete | /restore | /permanent | /move |
list_media_folders, create_media_folder, update_media_folder, delete_media_folder | /v1/media/folders CRUD |
add_media_to_folder, remove_media_from_folder | POST / DELETE /v1/media/folders/:id/items |
share_media_folder, unshare_media_folder | POST /v1/media/folders/:id/share, DELETE …/share/:user_id |
move_folder_contents | POST /v1/media/folders/:id/move-contents |
Differences worth knowing:
upload_mediaaccepts inputs the HTTP route rejects. It takessource(a public URL or an absolute local path) orsource_base64+filename, resolves it to bytes on the client, and posts the multipart form for you — the route itself only ever accepts a multipart file. Keepsource_base64under roughly 10 MB and always send afilenamewith an extension, because the extension is what picks the content type.media_upload_widgetis the upload-ticket flow. It mints the ticket and renders an upload card; the resulting CDN URLs arrive in a follow-up message.max_filesdefaults to10and is clamped to 1–20,media_typesrestricts the card to any ofimage/video/audio/document,purposesets the card's title, andproject_idfiles the results into a project. On hosts that cannot render widgets it returns a hint to useupload_mediainstead. This is also the only path that stores realdocumentitems.list_mediaexposes fewer filters than the route.preset_id,moodboard_id,source, andinclude_deletedare HTTP-only — the trash cannot be listed from the MCP tool.unshare_media_foldertakesuser_id, matching the:user_idpath segment; get the value from the folder'sshared_witharray.
Related
Cinematic Presets
Layer a deliberate photographic "Cinema mode" treatment onto image generations and edits, and discover the generation preset catalog.
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.