Kolbo Review
Frame.io-style client review over the API — upload media, create review assets with versions, comment with timecodes, set status, and mint guest share links.
Kolbo Review is the in-app Frame.io-style review layer: versioned assets, status workflow, timestamped comments, collections (folders), and guest share links. The Developer API exposes the same surface under /api/v1/review/* so API clients and @kolbo/mcp agents can drive review without opening the app.
Upload first with the Media Library (POST /v1/media/upload, upload ticket, or MCP upload_media / create_upload_ticket / media_upload_widget), then pass the returned media_id into Review. There is no separate Review uploader.
Endpoint map
GET /api/v1/review/storage-usage
POST /api/v1/review/assets
GET /api/v1/review/assets
GET /api/v1/review/assets/:id
PATCH /api/v1/review/assets/:id
POST /api/v1/review/assets/:id/versions
POST /api/v1/review/assets/:id/status
DELETE /api/v1/review/assets/:id
GET /api/v1/review/assets/:id/comments
POST /api/v1/review/assets/:id/comments
POST /api/v1/review/collections
GET /api/v1/review/collections
PATCH /api/v1/review/collections/:id
DELETE /api/v1/review/collections/:id
POST /api/v1/review/assets/:id/share-links
POST /api/v1/review/collections/:id/share-links
GET /api/v1/review/share-links
POST /api/v1/review/share-links/:linkId/revoke
POST /api/v1/review/comments/:noteId/reply
PATCH /api/v1/review/comments/:noteId
DELETE /api/v1/review/comments/:noteId
POST /api/v1/review/comments/:noteId/resolve
POST /api/v1/review/comments/:noteId/unresolveAll Review routes share the 120/min media-library rate-limit bucket (keyed by authenticated user — see Errors & Limits).
Permissions & limits
| Action | Required access |
|---|---|
| Get one asset, list/create comments, reply, resolve / unresolve | Project view+ |
List assets / collections with an explicit project_id | Project edit+ (the SDK project resolver rejects view-only projects with 403 SDK_PROJECT_ACCESS_DENIED) |
| Create / update assets & collections, add versions, set status, create / list share links | Project edit+ |
| Delete an asset or collection | Its owner (creator), or project full+ |
| Revoke a share link | The link's creator, or project edit+ |
| Edit a comment | Comment author only |
| Delete a comment | Comment author, or project full+ |
Additional product limits (same as the app):
| Limit | Detail |
|---|---|
| Free plan | Max 3 live review assets per owner without an active paid monthly/annual subscription (403 REVIEW_FREE_LIMIT) |
| Storage | 5 GB of Review version media per owner (413 REVIEW_STORAGE_LIMIT). Counted against the asset owner, even when another editor adds the version. |
| Per-file upload | Media upload rules still apply (Review versions reuse Media Library items) |
Guest share-link visitors use the public /review-link/:token app page — they are not authenticated via API key. The SDK only manages links as the project editor.
Typical flow
1. POST /v1/media/upload → media_id
2. POST /v1/review/assets { name, media_id, project_id } → asset (+ v1)
3. POST /v1/review/assets/:id/comments { body, time_start? }
4. POST /v1/review/assets/:id/status { review_status: "needs_review" }
5. POST /v1/review/assets/:id/share-links → share_url for the clientStorage usage
GET /api/v1/review/storage-usage{
"success": true,
"used_bytes": 118845242,
"cap_bytes": 5368709120
}Scoped to the API-key owner (the same cap that gates create / add-version). used_bytes sums the media size of every version across your non-deleted review assets.
Assets
Create (asset + v1 in one call)
POST /api/v1/review/assets| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (trimmed, truncated to 500 chars). |
media_id | string | Yes | MediaLibraryItem id from upload / list media. Must be owned by you or live in the same project_id. One media item per call — add more as versions. |
project_id | string | No | Target project (edit+). Omitted → auto-created "API Generations" project. |
collection_id | string | No | Optional Review collection folder. Must belong to the same project (404 otherwise). |
version_note | string | No | Note on v1 (truncated to 1000 chars). |
curl -X POST https://api.kolbo.ai/api/v1/review/assets \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Hero cut v1",
"media_id": "6a7b42c150af38464a4ff468",
"project_id": "6a098ebadfe025fd179a074a"
}'Response (201):
{
"success": true,
"asset": {
"id": "6a7b463b2e9279d87f9ba2e2",
"name": "Hero cut v1",
"project_id": "6a098ebadfe025fd179a074a",
"collection_id": null,
"review_status": "in_progress",
"current_version_index": 0,
"versions": [
{
"id": "…",
"label": "v1",
"media_id": "6a7b42c150af38464a4ff468",
"url": "https://media.kolbo.ai/…",
"media_type": "video",
"thumbnail_url": "https://media.kolbo.ai/…",
"note": null,
"uploaded_by": "6a01f0c2a51cce019cae1001",
"uploaded_at": "2026-08-11T15:56:00.000Z"
}
],
"cover_url": null,
"owner_id": "6a01f0c2a51cce019cae1001",
"status_changed_at": null,
"created_at": "2026-08-11T15:56:00.000Z",
"updated_at": "2026-08-11T15:56:00.000Z"
}
}List
GET /api/v1/review/assets| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | No | Project to list (edit+; alias project). Omitted → your default "API Generations" project (not every accessible project). |
collection_id | string | No | Filter to one collection (alias collection). |
status | string | No | in_progress | needs_review | approved | changes_requested. Also accepted as review_status. |
page | number | No | 1-indexed. Default 1. |
limit | number | No | Default 20, min 1, max 100. |
Sorted by most recently updated first.
{
"success": true,
"assets": [ /* asset shapes */ ],
"pagination": { "page": 1, "limit": 20, "total": 3, "pages": 1 }
}Get / update / delete
GET /api/v1/review/assets/:id
PATCH /api/v1/review/assets/:id
DELETE /api/v1/review/assets/:idGET needs view+; PATCH needs edit+. Both return { "success": true, "asset": { … } }. The PATCH, add-version and set-status responses do not populate version media, so each version's url, media_type and thumbnail_url come back null there — GET the asset for those fields.
PATCH body (all optional):
| Field | Type | Description |
|---|---|---|
name | string | Rename (cannot be empty). |
collection_id | string | null | Move into a collection in the same project, or null to uncollect. |
current_version_index | number | Switch the active version (0-based; must be < versions.length). |
DELETE (asset owner or full+) soft-deletes the asset and all comments on its versions, returning { "success": true }. It stops counting toward the free-tier limit and storage usage immediately. Media you uploaded through the Media Library stays in your library; only media created by the in-app Review uploader (and not used by another live review asset) is removed.
Add a version
POST /api/v1/review/assets/:id/versions| Field | Type | Required | Description |
|---|---|---|---|
media_id | string | Yes | New MediaLibraryItem to append as v{n+1}. Same rule as create: owned by you or in the asset's project. |
version_note | string | No | Optional note (truncated to 1000 chars). |
Becomes the current version automatically. Returns 201 with { "success": true, "asset": { … } }.
Set status
POST /api/v1/review/assets/:id/status| Field | Type | Required | Description |
|---|---|---|---|
review_status | string | Yes | in_progress | needs_review | approved | changes_requested. Also accepted as status. |
Returns { "success": true, "asset": { … } } with the new review_status and status_changed_at. Notifies the asset owner when the changer is someone else (same as the app).
Collections
Folders that group review assets inside a project.
POST /api/v1/review/collections
GET /api/v1/review/collections
PATCH /api/v1/review/collections/:id
DELETE /api/v1/review/collections/:idCreate body: { "name": string, "project_id": string } — both required (400 VALIDATION without project_id; there is no default project here), edit+. Name is truncated to 200 chars. Returns 201.
List query: project_id optional (alias project; defaults to API Generations, same as assets). Not paginated; most recently updated first.
Patch body: { "name": string } (required, truncated to 200 chars), edit+.
Delete: collection owner or full+. Soft-delete; assets in that folder become uncollected (collection_id: null).
Collection shape:
{
"success": true,
"collection": {
"id": "6a7b47e1a51cce019cae9701",
"name": "Launch cuts",
"project_id": "6a098ebadfe025fd179a074a",
"cover_url": null,
"owner_id": "6a01f0c2a51cce019cae1001",
"created_at": "2026-08-11T15:57:00.000Z",
"updated_at": "2026-08-11T15:57:00.000Z"
}
}List returns { "success": true, "collections": [ … ] }.
Comments
Text comments (optional video timecodes) on a review asset's media. Drawing annotations and voice notes stay app-only for now.
List / create on an asset
GET /api/v1/review/assets/:id/comments
POST /api/v1/review/assets/:id/commentsList query: optional version_media_id (defaults to the asset's current version media; must be the media_id of one of the asset's versions, else 400). The list is flat — top-level comments and replies together (replies carry parent_note_id), sorted by time_start then creation time. Not paginated.
Create body:
| Field | Type | Required | Description |
|---|---|---|---|
body | string | Yes (in practice) | Plain-text comment, max 5000 chars. The server does not reject an empty body, but the comment would be blank. |
time_start | number | No | Seconds on the timeline. Omit for a general comment. |
time_end | number | No | End of a range in seconds; must be >= time_start or the comment is rejected. |
version_media_id | string | No | Comment on a non-current version's media (a version's media_id). |
curl -X POST https://api.kolbo.ai/api/v1/review/assets/6a7b463b2e9279d87f9ba2e2/comments \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "body": "Cut the logo sting shorter", "time_start": 3.2 }'Comment shape (create returns 201 with comment; list returns comments):
{
"success": true,
"comment": {
"id": "6a7b4810a51cce019cae9720",
"body": "Cut the logo sting shorter",
"author": { "id": "6a01f0c2a51cce019cae1001", "first_name": "Dana", "last_name": "Levi", "avatar_url": null },
"author_kind": "user",
"time_start": 3.2,
"time_end": null,
"resolved": false,
"reply_count": 0,
"parent_note_id": null,
"media_id": "6a7b42c150af38464a4ff468",
"created_at": "2026-08-11T15:59:00.000Z",
"updated_at": "2026-08-11T15:59:00.000Z",
"edited_at": null
}
}Reply / edit / delete / resolve
POST /api/v1/review/comments/:noteId/reply
PATCH /api/v1/review/comments/:noteId
DELETE /api/v1/review/comments/:noteId
POST /api/v1/review/comments/:noteId/resolve
POST /api/v1/review/comments/:noteId/unresolveReply body: { "body": string }. One level of threading only — replying to a reply returns 400 NESTED_REPLY_FORBIDDEN. Returns 201 with the reply as comment.
Edit body: optional body, time_start, time_end (author only, else 403 NOT_AUTHOR). Sets edited_at.
Delete: author, or full+ on the project (else 403 NOT_AUTHORIZED). Also deletes the comment's replies. Returns { "success": true }.
Resolve / unresolve: view+; returns the updated comment.
Share links
Mint a guest URL the client opens without a Kolbo account.
POST /api/v1/review/assets/:id/share-links
POST /api/v1/review/collections/:id/share-links
GET /api/v1/review/share-links?target_type=asset|collection&target_id=…
POST /api/v1/review/share-links/:linkId/revokeCreate body (all optional; you need edit+ on the project). Returns 201.
| Field | Type | Default | Description |
|---|---|---|---|
role_label | string | "Client" | Label shown to guests (truncated to 40 chars). |
permissions | object | see below | Boolean flags; unknown keys and non-boolean values are ignored, omitted keys take their default. |
require_email | boolean | true | Ask guests for an email before commenting. |
password | string | none | Optional password gate (stored hashed, never returned). |
allowed_emails | string[] | [] | Optional allow-list. Entries without @ are dropped; stored lowercased. |
expires_at | string | never | ISO-8601 expiry; must be in the future (400 VALIDATION). |
permissions keys and defaults: canComment (true), canDownload (false), canViewOtherComments (true), canResolveOwn (false), canSwitchVersions (true), canSetStatus (false).
List query: target_type (asset | collection) and target_id are both required (400 VALIDATION otherwise); edit+. Returns { "success": true, "share_links": [ … ] }, newest first. The list includes revoked links: check revoked (boolean). revoked_at is the revocation time (for a revoked link, the time the link was last updated), null while the link is active.
Revoke returns { "success": true }.
{
"success": true,
"share_link": {
"id": "6a7b4797a51cce019cae9685",
"share_url": "https://app.kolbo.ai/review-link/76642589a6c554d156e16f33b8108ba0…",
"target_type": "asset",
"target_id": "6a7b463b2e9279d87f9ba2e2",
"project_id": "6a098ebadfe025fd179a074a",
"role_label": "Client",
"permissions": {
"canComment": true,
"canDownload": false,
"canViewOtherComments": true,
"canResolveOwn": false,
"canSwitchVersions": true,
"canSetStatus": false
},
"require_email": true,
"expires_at": null,
"revoked": false,
"revoked_at": null,
"created_at": "2026-08-11T15:58:00.000Z"
}
}Treat share_url as a credential. Only editors can list links; editors or the link's creator can
revoke. Revoking immediately invalidates the guest page. share_url carries attribution query
parameters — use it exactly as returned.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION | Missing name / media_id / collection project_id, bad ids, invalid status, out-of-range current_version_index, past expires_at, missing share-link list query, no media version on the asset. |
| 400 | SDK_PROJECT_INVALID_ID | project_id is not a valid id. |
| 400 | INVALID_NOTE | Comment validation failed (e.g. body over 5000 chars). |
| 400 | NESTED_REPLY_FORBIDDEN | Replying to a reply. |
| 400 | INVALID_NOTE_ID | Malformed comment id. |
| 403 | FORBIDDEN / INSUFFICIENT_PERMISSION | No access to the project / permission tier too low. |
| 403 | SDK_PROJECT_ACCESS_DENIED | project_id given but you have less than edit on it. |
| 403 | NOT_AUTHOR / NOT_AUTHORIZED | Editing someone else's comment / deleting it without full+. |
| 403 | PROJECT_ACCESS_DENIED / NOTE_ACCESS_DENIED | No access to the project of the comment's media (or, for media outside any project, you are not its owner). |
| 403 | REVIEW_FREE_LIMIT | Free plan already has 3 live review assets. |
| 404 | NOT_FOUND | Asset, collection, media, or link not found / not accessible. |
| 404 | SDK_PROJECT_NOT_FOUND | Project not found or not accessible. |
| 404 | NOTE_NOT_FOUND | Comment not found or deleted. |
| 413 | REVIEW_STORAGE_LIMIT | Owner would exceed the 5 GB Review storage cap. |
| 429 | — | Over the 120/min bucket. |
Envelope: { "success": false, "error": "…", "code": "…" }.
MCP tools
Matching @kolbo/mcp tools (v1.66.0+):
| Tool | Maps to |
|---|---|
get_review_storage_usage | GET /v1/review/storage-usage |
list_review_assets / get_review_asset / create_review_asset / update_review_asset / add_review_version / set_review_status / delete_review_asset | Assets |
list_review_collections / create_review_collection / update_review_collection / delete_review_collection | Collections |
list_review_comments / create_review_comment / reply_review_comment / edit_review_comment / delete_review_comment / resolve_review_comment / unresolve_review_comment | Comments |
create_review_share_link / list_review_share_links / revoke_review_share_link | Share links |
Intended agent flow:
1. upload_media | create_upload_ticket | media_upload_widget → media_id
2. (user named a project?) list_projects → project_id
3. create_review_asset { name, media_id, project_id }
4. create_review_comment / set_review_status / create_review_share_link as neededOut of scope (v1)
Drawing annotations / pins, voice-note comments, storyboard share links, live cursors, and guest-token auth for MCP. Use the Kolbo app for those.