Kolbo.AIKolbo.AI Docs
Developer API

Visual DNA

Create and manage Visual DNA presets for character/product/style consistency across generations.

Visual DNA captures the visual identity of a character, product, style, scene, or environment from reference media. Once created, you can attach Visual DNAs to image and video generations to maintain consistency across outputs.

At generation time Kolbo packs every still on each attached DNA into the model's remaining image slots (user uploads first, then one still per DNA, then leftovers round-robin). If a DNA only gets one leftover slot and has no distinct character sheet, unused stills become a white grid. Keep each profile one identity + one vibe: no extra heroes on character DNAs, no main characters inside environment DNAs (anonymous crowd is fine).

The create endpoint forwards your body straight to the in-app Visual DNA controller, which reads camelCase field names: dnaType, promptHelper, characterSheetUrl. Snake_case variants (dna_type, prompt_helper) are silently ignored — a DNA sent with dna_type=product is created as a character. The MCP tool create_visual_dna maps its snake_case args to these names for you.

Create Visual DNA

Upload reference media and create a Visual DNA profile. The API analyzes the references and generates a reusable identity profile.

Endpoint

POST /api/v1/visual-dna

Rate limit: 30 requests/minute per user (shared SDK generation bucket).

Request Body (multipart/form-data, or JSON when every reference is a URL)

FieldTypeRequiredDescription
namestringYes1–100 chars after trimming. Rejected if it matches Image1, Image2, … (case-insensitive; reserved for image references). If you already own an active DNA with this name (case-insensitive) the server auto-appends (1), (2), … rather than failing. Any language/script is accepted.
project_idstringNoCreate the DNA inside a project so teammates with edit/full/owner access can list, fetch and generate with it (they pass the same project_id on list/get). projectId is accepted as an alias. You need edit access yourself — otherwise 403 with code: "ACCESS_DENIED". Omit for a private DNA.
shareWithOrganizationbooleanNotrue (or the string "true" / "organization") shares the DNA with your active organization (content_scope: "organization"). Opt-in; default is personal. Ignored if you have no active organization.
dnaTypestringNocharacter | product | style | environment | scene. Default: character. Any other value is not caught up front — it fails on save, after the references have been uploaded and analyzed, as a 500 with code: "DATABASE_ERROR".
promptHelperstringNoFree-text description/notes that steer the DNA extraction.
imagesfile(s)No*Up to 4 reference image files (multer maxCount: 4).
videosfileNo*Up to 1 reference video file.
audiofileNo*Up to 1 reference audio file.
mediaarray or JSON stringNo*URL-based references instead of (or mixed with) file parts: [{ "type": "url", "url": "…", "mediaType": "image" }]. type is url | file; mediaType is image | video | audio (default image). Each entry is an object; a bare http(s) URL string is also accepted and treated as { "type": "url", "url": "…", "mediaType": "image" }, and an object with a url but no type is treated as type: "url". URL references are only honoured for mediaType: "image" — video/audio URLs are skipped with a warning; upload those as files. A type: "file" entry is paired with the next file part of the same mediaType; file parts not described in media are appended after it. Duplicate image URLs (same path, CDN or origin) are collapsed to one.
images (legacy, non-file)array or JSON stringNo*Legacy alias for media (used only when media is absent) — same object shape; items without mediaType default to image.
characterSheetUrlstringNoURL of a reference sheet (see Reference sheet) to set as this DNA's primary reference. Works for every dnaType.
voiceSourcestringNoclone | assign | design. Character DNAs only — ignored for every other dnaType. When omitted, the server infers clone if an audio reference was supplied and skips voice resolution otherwise. clone builds an ElevenLabs voice from the uploaded audio clip; assign points the DNA at an existing voice; design synthesizes a new voice from the DNA's main image plus promptHelper and charges credits (model fal_elevenlabs_voice_design — read its cost from GET /api/v1/models).
assignedVoiceIdstringNoRequired when voiceSource is assign, ignored otherwise. Pass the voice_id exactly as GET /api/v1/voices returns it — that endpoint already emits custom_<id> for your own custom voices and the raw library id for preset voices. An unresolvable value fails the (non-blocking) voice step and the DNA is created without a voice.

* At least one reference (image, video, or audio — file or image URL) is required; a request with none returns 400.

Uploaded parts are buffered in memory with a 1 GB per-file ceiling and no MIME-type filter on this route. The multipart handler is registered with named fields (images ×4, videos ×1, audio ×1), so 6 file parts is the real ceiling — extra parts under those names are rejected by multer. The 4/1/1 caps apply to file parts only; there is no server-side cap on the number of image URLs passed via media.

Voice resolution is non-blocking: if the clone/assign/design step fails, the failure is swallowed and the Visual DNA is still created, carrying a voice record with an error status. The POST response does not include the voice; read it back with GET /v1/visual-dna/:id (or the list), which returns a voice object (source, status, name, assigned_voice_name, provider, language) when one was resolved, has_voice_reference: true when the DNA carries a usable voice reference, and singing_voice (status, name) when a singing voice exists. (kling_voice_id is unrelated: it comes from the Kling provider adapter, not from voiceSource.)

Character attribute fields (gender, ethnicity, ethnicityPreset, bodyType, bodyTypePreset, hairColor, eyeColor, skinTone, ageRange, ageRangePreset, specificAge) are also read on create (camelCase only), and are only stored when dnaType is character. Enums: gender male | female | not-specified; ethnicityPreset caucasian | african | asian | hispanic | middle-eastern | mixed | custom | not-specified; bodyTypePreset slim | athletic | average | plus-size | muscular | petite | curvy | fantasy | custom | not-specified; ageRangePreset toddler | child | teenager | young-adult | adult | middle-aged | senior | not-specified; specificAge is a number 0–120. The others are free text. An out-of-enum value fails on save (500 DATABASE_ERROR). The MCP tool does not expose them on create.

Example

curl -X POST https://api.kolbo.ai/api/v1/visual-dna \
  -H "X-API-Key: kolbo_live_..." \
  -F "name=emma" \
  -F "dnaType=character" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "promptHelper=Young woman with red hair"

URL-only creation (no multipart needed):

curl -X POST https://api.kolbo.ai/api/v1/visual-dna \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "emma",
    "dnaType": "character",
    "media": [
      { "type": "url", "url": "https://cdn.kolbo.ai/....jpg", "mediaType": "image" }
    ]
  }'

Response

{
  "success": true,
  "visual_dna": {
    "id": "6601a1b2c3d4e5f6a7b8c9d0",
    "name": "emma",
    "dna_type": "character",
    "created_at": "2026-03-06T12:00:00Z",
    "description": "AI-generated visual analysis...",
    "thumbnail_url": "https://cdn.kolbo.ai/visual-dna/...",
    "images": ["https://cdn.kolbo.ai/..."],
    "videos": [],
    "audio_clips": [],
    "sheet_url": "https://cdn.kolbo.ai/..."
  }
}

Visual DNA creation is synchronous — the request stays open while the references are uploaded and analyzed. Success is HTTP 201. description here is whitespace-collapsed and clipped to 400 characters (fetch the DNA for the full text). thumbnail_small_url appears only when a small display thumbnail already exists (it is built in the background after create, so usually not on this response); sheet_url only when you passed characterSheetUrl. If name collided with one of your DNAs, name carries the auto-suffixed value.

Errors

Failures come back as { "success": false, "error": "…", "code": "…" }:

StatuscodeMeaning
400INVALID_VISUAL_DNA_NAMEName over 100 chars or matching Image<n>.
400VISUAL_DNA_ERRORName missing, or no reference supplied (no files and no usable media/images entries). Read error for the specific message.
403ACCESS_DENIEDproject_id given, but you do not have edit access to that project.
422ANALYSIS_FAILEDThe vision model declined the reference images — retry with different references.
500ANALYSIS_FAILEDThe analysis pass returned an empty result. Same code as the 422, different status — branch on the code, not the status, if you want to retry with new references.
500UPLOAD_FAILEDA reference file/URL could not be uploaded or transcoded.
500DATABASE_ERRORSave failed, e.g. an out-of-enum dnaType.
500UNEXPECTED_ERRORAnything else (provider error, unhandled exception).

Naming: pick a single-token name

The name you give a DNA is also the token you type as @Name inside a prompt (see Prompt tagging). The @ capture runs until a double space, the next @, punctuation, or end of string, so a multi-word name in the middle of a sentence over-captures the words after it. The server recovers by retrying progressively shorter prefixes, but a short, lowercase, single token — emma, tokyo_neon, brand_red — resolves on the first try and never collides with surrounding prompt text.

List Visual DNAs

Returns all Visual DNAs accessible to your account. By default this includes your personal DNAs, global system presets (cast characters, etc.), and any organization-shared DNAs. Use query params to filter.

Endpoint

GET /api/v1/visual-dna

Query Parameters

ParameterTypeDescription
scopestringFilter by ownership. personal (your own), global (system presets/cast), organization (org-shared), mine (personal + organization + shared-project, globals excluded), or all. Omit for everything accessible. Matched case-insensitively after trimming; an unrecognised value behaves like all.
searchstringCase-insensitive match on name, tags, or description. A term starting with a Latin letter, digit or _ is anchored to a word start (man does not match inside woman); other scripts (Hebrew, Arabic, …) match as a substring. When set, name hits are sorted ahead of tag/description hits.
collectionstringFilter global presets by collection: cast, influencers, props, locations, styles, street. (glamour is NSFW-gated: it returns an empty list, and its presets are hidden from every scope and return 404 on get/import, unless your account is NSFW-approved.)
tagsstringComma-separated tags to filter by (OR logic)
project_idstringA project you have edit, full, or owner permission on. Adds that project owner's personal Visual DNAs (and teammates' DNAs created in it) to the result, each flagged with an owner object and is_mine: false. View-only members and non-members get no extra items.
pageinteger1-based page. Paging is applied only when page or limit is sent; otherwise the full list is returned.
limitintegerPage size, 1–100, default 50 (only when paging).

Omitting scope returns everything accessible, which includes Kolbo's large library of global cast/preset DNAs. Pass scope=personal when you only want the DNAs the account created — that is what the MCP tool list_visual_dnas defaults to.

scope=personal and scope=organization are read from the database as mine (globals excluded), then split into personal vs organization in memory. scope=global reads only the preset catalog, and scope=global alone (no search, collection, or tags) is served from a 15-minute cache, so it is the fastest way to browse the preset catalog. Paging (page/limit) is also applied in memory after the scope split, so it reduces payload size, not query cost.

Example

# List only your personal Visual DNAs
curl "https://api.kolbo.ai/api/v1/visual-dna?scope=personal" \
  -H "X-API-Key: kolbo_live_..."

# Search global cast characters
curl "https://api.kolbo.ai/api/v1/visual-dna?scope=global&collection=cast&search=emma" \
  -H "X-API-Key: kolbo_live_..."

Response

{
  "success": true,
  "visual_dnas": [
    {
      "id": "6601a1b2c3d4e5f6a7b8c9d0",
      "name": "Emma",
      "dna_type": "character",
      "description": "AI-generated visual analysis...",
      "thumbnail_url": "https://cdn.kolbo.ai/...",
      "images": ["https://cdn.kolbo.ai/..."],
      "videos": [],
      "audio_clips": [],
      "is_global": false,
      "content_scope": "personal",
      "preset_collection": null,
      "tags": ["portrait", "redhead"],
      "generation_status": "ready",
      "folder_id": null,
      "sheet_url": "https://cdn.kolbo.ai/...",
      "owner": { "userId": "65ab…", "name": "", "avatar": null },
      "is_mine": true,
      "gender": "female",
      "ethnicity_preset": "caucasian",
      "hair_color": "red",
      "eye_color": "green",
      "age_range_preset": "young-adult",
      "created_at": "2026-03-06T12:00:00Z"
    }
  ],
  "count": 1,
  "total": 1,
  "has_more": false
}

Field notes:

  • count is the number of items in this response; total is the number after scope/collection filtering and before paging; has_more is true when a later page exists (always false without page/limit).
  • description on the list endpoint is whitespace-collapsed and clipped to 400 characters; GET /v1/visual-dna/:id returns the full text.
  • images is the union of the DNA's stills and any stills recorded in its image inventory (deduplicated, data: URIs dropped). thumbnail_small_url (a small display thumbnail) is present when one has been built.
  • owner / is_mine — attribution. is_mine is true only for a non-global DNA you created; global presets and DNAs another user created come back with is_mine: false and owner.userId set to their creator; name and avatar are filled only for the project owner's DNAs surfaced through project_id (other teammates' DNAs and your own items carry an empty name and null avatar on the list endpoint — use GET /v1/visual-dna/:id for the resolved owner).
  • sheet_url — present for any dna_type when the DNA has a stored reference sheet (and the stored value is not an inline data: URI).
  • Character-specific fields (gender, ethnicity, ethnicity_preset, body_type, body_type_preset, hair_color, eye_color, skin_tone, outfit, age_range_preset, specific_age) are included only for dna_type: "character".
  • Voice fields: voice, has_voice_reference and singing_voice appear only when present (see the voice callout under Create). kling_voice_id is included when the Kling adapter registered a voice. is_shared is included when the DNA has been shared via a public link.

Get Visual DNA

Endpoint

GET /api/v1/visual-dna/:id

Query Parameters

ParameterTypeDescription
project_idstringRequired to read a teammate's Visual DNA that lives in a shared project — you must have edit, full, or owner permission on it.

A malformed id returns 400; no access returns 403 with code: "ACCESS_DENIED", missing returns 404 with code: "NOT_FOUND". A preset in an NSFW-gated collection returns 404 unless your account is NSFW-approved.

Example

curl https://api.kolbo.ai/api/v1/visual-dna/6601a1b2c3d4e5f6a7b8c9d0 \
  -H "X-API-Key: kolbo_live_..."

Response

Returns the same fields as the list endpoint for a single Visual DNA, except that description is the full stored text (not clipped), plus updated_at and a resolved owner (with the owner's real name and avatar).

{
  "success": true,
  "visual_dna": {
    "id": "6601a1b2c3d4e5f6a7b8c9d0",
    "name": "emma",
    "dna_type": "character",
    "description": "AI-generated visual analysis...",
    "thumbnail_url": "https://cdn.kolbo.ai/...",
    "images": ["https://cdn.kolbo.ai/..."],
    "videos": [],
    "audio_clips": [],
    "is_global": false,
    "content_scope": "personal",
    "preset_collection": null,
    "tags": ["portrait", "redhead"],
    "generation_status": "ready",
    "folder_id": null,
    "sheet_url": "https://cdn.kolbo.ai/...",
    "owner": { "userId": "65ab…", "name": "Dana Levi", "avatar": "https://…" },
    "is_mine": true,
    "gender": "female",
    "ethnicity_preset": "caucasian",
    "hair_color": "red",
    "eye_color": "green",
    "age_range_preset": "young-adult",
    "created_at": "2026-03-06T12:00:00Z",
    "updated_at": "2026-03-07T09:30:00Z"
  }
}

The DNA's internal systemPrompt (the extracted identity prompt) is deliberately not returned by any SDK endpoint.

Update Visual DNA

Edit an existing personal Visual DNA in place. Do not delete and recreate to rename, change the description, swap stills, or attach a new sheet — the old id is what generations and @Name bindings already point at.

PUT /api/v1/visual-dna/:id

Rate limit: 30 requests/minute per user (shared SDK generation bucket).

Accepts JSON for metadata-only updates, or the same multipart fields as POST /v1/visual-dna when replacing references. Sending any reference (images/videos/audio file parts, or media / legacy images entries) replaces the whole reference set — stills, video and audio together — so re-send the video/audio you want to keep alongside new stills. Omit all of them to keep the current references. Snake_case and camelCase are both accepted on this route (camelCase wins when both are sent).

FieldNotes
nameNew display name. A changed name is validated like create (1–100 chars, not Image<n>) — otherwise 400 with code: "INVALID_VISUAL_DNA_NAME". An empty or omitted name keeps the current one. On a metadata-only update a collision with another of your DNAs is auto-suffixed (n)
prompt_helper / promptHelperDescription / intent notes. Replaces the old text. When it changes on an analyzed DNA, the identity prompt is re-synthesized from it (no re-upload needed)
dna_type / dnaTypecharacter / style / product / scene / environment. Changing type re-analyzes
character_sheet_url / characterSheetUrlNew primary sheet URL. A changed sheet re-analyzes when references are also sent
remove_character_sheet / removeCharacterSheettrue (or "true") clears the stored sheet. Without it, a DNA with no sheet falls back to its first still
images (files) / videos / audioReplacement reference files — same 4/1/1 file-part caps as create
media / images (non-file)Replacement references, e.g. [{ "type": "url", "url": "…", "mediaType": "image" }] — same shape and image-URL-only rule as create. A bare http(s) URL string, or an object with a url but no type, is treated as an image URL reference; any other non-object entry returns 400 (Invalid media item…) and leaves the DNA unchanged
isActivetrue restores a soft-deleted DNA (see Delete). Applied only on a metadata-only update — ignored when references or dnaType change in the same request
systemPromptWrite-only override of the extracted identity prompt. Applied only when it differs from the stored one and no stills/sheet/type change in the same request; it also suppresses the promptHelper re-synthesis. Never returned
gender, ethnicity, ethnicity_preset, body_type, body_type_preset, hair_color, eye_color, skin_tone, age_range, age_range_preset, specific_age (or their camelCase forms)Optional character attributes, applied only to character DNAs. Same enums as create; an empty value resets a preset to not-specified
curl -X PUT https://api.kolbo.ai/api/v1/visual-dna/6601a1b2c3d4e5f6a7b8c9d0 \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"maya","prompt_helper":"late-20s woman, short black hair, warm skin"}'

Response shape matches GET /v1/visual-dna/:id. Owner only: a DNA you do not own, or a global preset, returns 404 with code: "NOT_FOUND" (import a preset first — see below). A malformed id, or a media/images string that is not valid JSON, returns 400. A re-analysis the vision model declines returns 422 with code: "ANALYSIS_FAILED"; any other failure is a 500 with code: "VISUAL_DNA_ERROR" (the specific message is in error). MCP: update_visual_dna.

Import a Visual DNA

Copy a global preset — or a teammate's DNA from a project you share — into your own personal library, so you can edit it, file it in a folder, or keep it if the source changes. Free (no credits).

POST /api/v1/visual-dna/:id/import

Rate limit: 120 requests/minute per user (shared SDK media bucket).

FieldTypeRequiredDescription
project_idstringFor non-global sourcesA project owned by the source DNA's owner that you are a member of. projectId is accepted as an alias. Omit for global presets.
curl -X POST https://api.kolbo.ai/api/v1/visual-dna/6601a1b2c3d4e5f6a7b8c9d0/import \
  -H "X-API-Key: kolbo_live_..."
{
  "success": true,
  "message": "Visual DNA preset imported successfully",
  "visual_dna": {
    "id": "6602b2c3d4e5f6a7b8c9d0e1",
    "name": "Emma",
    "dna_type": "character",
    "description": "AI-generated visual analysis...",
    "thumbnail_url": "https://cdn.kolbo.ai/...",
    "images": ["https://cdn.kolbo.ai/..."],
    "is_global": false,
    "content_scope": "personal",
    "source_id": "6601a1b2c3d4e5f6a7b8c9d0",
    "created_at": "2026-03-06T12:00:00Z"
  }
}

The copy gets a new id, keeps the source's stills, video, audio, sheet and character attributes, and is named after the source with (n) appended if you already have DNAs by that name. Errors: 400 (no code) for a malformed id; 400 (IMPORT_ERROR) for a missing project_id on a non-global source or an invalid project_id; 403 (FORBIDDEN) when you are not a member of a project owned by the source's owner; 404 (NOT_FOUND) when the source does not exist or is in an NSFW-gated collection you cannot access; 409 (ALREADY_EXISTS) on a duplicate.

Delete Visual DNA

Endpoint

DELETE /api/v1/visual-dna/:id

Example

curl -X DELETE https://api.kolbo.ai/api/v1/visual-dna/6601a1b2c3d4e5f6a7b8c9d0 \
  -H "X-API-Key: kolbo_live_..."

Response

{
  "success": true,
  "message": "Visual DNA deleted successfully"
}

Only the owner can delete. Deletion is a soft delete: the DNA disappears from the list, but the record is kept so past generations still resolve it (GET /v1/visual-dna/:id still returns it); the owner can restore it with PUT /v1/visual-dna/:id and { "isActive": true }. Failures come back as success: false with code of FORBIDDEN (403 — global presets cannot be deleted), NOT_FOUND (404 — missing, already deleted, or not yours), or DELETE_ERROR. A malformed id returns 400.

Reference sheet (stronger consistency)

A reference sheet is the single biggest consistency booster for a Visual DNA — the same step the in-app Visual DNA wizard offers. It works for every DNA type, not just characters. Generate one from your reference image URLs, then attach it when creating (or updating) the DNA.

POST /api/v1/visual-dna/character-sheet

Rate limit: 30 requests/minute per user (shared SDK generation bucket).

FieldTypeRequiredDescription
image_urlsarray of stringYesNon-empty array of reference image URLs (no fixed count cap). imageUrls is accepted as an alias. Each URL is SSRF-validated — a private/internal host fails the request.
sheet_typestringNocharacter (3 panels in one row, 16:9: front full body, back full body, face close-up) | character_headless (same 3-panel row and 16:9, but panel 1 is a headless full-body front view; back view and face close-up are kept) | character_bible (dense production model sheet, 3:2: four-angle turnaround, faces, hair and outfit details, labeled color swatches) | product (6-panel grid: angles + branding/material/construction close-ups) | environment (6-panel grid: location angles + one signature detail) | style (6-panel grid: the same look on six varied subjects). sheetType is accepted as an alias. An unrecognised value silently falls back to character rather than erroring.

| model | string | No | Sheet model. Default gpt-image-2.5-flare/edit. Names containing gpt-image-2.5, flare or sunburst map to gpt-image-2.5-flare/edit / gpt-image-2.5-sunburst/edit; other GPT Image names map to gpt-image-2/edit (an id already ending in /edit is kept); names containing nano-banana map to nano-banana-2-image-editing. Only these sheet models are accepted after that mapping: gpt-image-2.5-flare/edit, gpt-image-2.5-sunburst/edit, gpt-image-2/edit and nano-banana-2-image-editing; anything else returns 400 MODEL_NOT_FOUND. Omit for the default. | | resolution | string | No | "4K" (also "2160" / "UHD"); anything else, or omitted, is "2K". | | project_id | string | No | Project to file the sheet's media item under (projectId is accepted as an alias). A project you cannot access is ignored and the media stays unassigned rather than failing the request. |

presetId from the in-app route is not read, so a sheet generated through the SDK is never auto-attached to an existing DNA. Attach it yourself with characterSheetUrl on POST /v1/visual-dna (or character_sheet_url on PUT).

Charges credits before generating. The model and credit cost are resolved at request time from the character-sheet model document — do not hardcode them; read the cost from credits_used on the response. Insufficient balance returns HTTP 403.

The SDK error envelope is { "success": false, "error": "…" }, plus a details object when the handler supplied one. The underlying code, required, and available fields the in-app route emits on an insufficient-credits 403 are not forwarded — branch on the HTTP status, not on a code field. A missing/empty image_urls is a 400. A generation failure (including an SSRF-rejected URL) comes back as 500 with the real cause in error and, when known, details.upstream_status and details.reservation_id.

Response:

{
  "success": true,
  "character_sheet_url": "https://…",
  "media_id": "6601a1b2c3d4e5f6a7b8c9d0",
  "credits_used": 12,
  "resolution": "2K"
}

Then pass that URL back as characterSheetUrl on POST /v1/visual-dna to make it the profile's primary reference. MCP: generate_character_sheet → create_visual_dna with character_sheet_url.

The MCP tool create_visual_dna resolves each images/video/audio entry (URL or absolute local path) to a buffer client-side and rejects anything over 25 MB per file before it reaches the API. That is an MCP-side guard, not a server rule — direct HTTP callers are bound only by the route's 1 GB per-part ceiling.

Folders — Organizing Your Characters

When a project accumulates many Visual DNAs (a large cast, multiple productions), organize them into user folders. Folders are flat (no nesting), unique-named per user, and hold personal DNAs only — global presets and organization DNAs cannot be placed in personal folders.

All five folder routes run on the shared SDK media bucket — 120 requests/minute per user (rate limits are keyed by user id, not by API key), counted together with the media-library routes.

List Folders

GET /api/v1/visual-dna/folders

Returns each folder with its live item_count:

{
  "success": true,
  "folders": [
    { "id": "65f1…", "name": "Main Cast", "color": "#FF5733", "item_count": 6, "created_at": "…", "updated_at": "…" }
  ],
  "count": 1
}

To list the DNAs inside a folder, call GET /v1/visual-dna and filter by the folder_id field returned on each profile.

Create a Folder

POST /api/v1/visual-dna/folders
FieldTypeRequiredDescription
namestringYesFolder name, trimmed. Unique per user — an exact duplicate returns 409. Empty/whitespace-only returns 400.
colorstringNoFree-form color string stored as-is; the app uses hex, e.g. #FF5733. Omitted or falsy → stored as null.

Returns 201 with { "success": true, "folder": { … } }.

Rename / Recolor a Folder

PUT /api/v1/visual-dna/folders/:folderId
FieldTypeRequiredDescription
namestringYesNew name. Always required — pass the current name to keep it. Duplicate name → 409.
colorstringNoNew color. Omit to leave unchanged; pass null or "" to clear it.

A non-ObjectId :folderId returns 400; a folder you do not own returns 404.

Delete a Folder

DELETE /api/v1/visual-dna/folders/:folderId

The DNAs inside are not deleted — they move back to root. Response: { "success": true, "id": "…", "items_moved_to_root": 6 }.

Move a DNA Into / Out of a Folder

PUT /api/v1/visual-dna/:id/folder
FieldTypeRequiredDescription
folder_idstring or nullYesTarget folder id, or null to move the DNA back to root. folderId is accepted as an alias. A non-ObjectId, non-null value returns 400.

Personal DNAs only: global presets return 403 ("import it first"), organization DNAs return 403, and anything else you do not own returns 404. A folder that is not yours returns 404. Response: { "success": true, "id": "…", "folder_id": "…" }.

MCP Tools

The matching @kolbo/mcp tools: list_visual_dna_folders, create_visual_dna_folder, update_visual_dna_folder, delete_visual_dna_folder, move_visual_dna_to_folder. Typical flow when building a large cast:

1. create_visual_dna_folder { name: "Film X Cast" }
2. create_visual_dna (per character)
3. move_visual_dna_to_folder { visual_dna_id, folder_id }   # file each as you go

Prompt tagging: @Name and #Name

Kolbo prompts support two mention conventions, and they are alternatives to the id fields, not a requirement on top of them:

  • @Name resolves a Visual DNA by name. On text-to-video (POST /v1/generate/video) and image-to-video (POST /v1/generate/video/from-image), @Name mentions are parsed into DNA ids only when visual_dna_ids is not supplied and at least one selected model reports supports_visual_dna: true. On video-to-video (POST /v1/generate/video-from-video) mentions are parsed whenever the prompt contains @ and no ids were sent; a recognised name is stripped from the prompt even on a model that cannot take the DNA, but its ids are only adopted on a DNA-capable model.
  • #Name resolves a moodboard by name. An explicit moodboard_id in the body always wins; a # token that does not resolve to a moodboard you can access is left untouched in the prompt.

Passing visual_dna_ids alone is enough — the server loads each DNA's reference images even when the prompt contains no @ mention. Use @Name when you want to control where in the sentence the identity lands ("@emma walks into frame"), or when you are composing prompts by name rather than by id.

The @ capture is greedy across single spaces: it runs from @ until a double space, the next @, punctuation (. , ! ?), or end of string. So @Sarah Johnson walks into frame. first captures the whole phrase, not just Sarah Johnson. When the captured text does not match a DNA, the server retries progressively shorter word prefixes (longest first) and trims the mention back to the matched name, so multi-word names still resolve — but single-token names avoid the round trip entirely. The parser also skips the reserved @Image1 / @Video1 / @Audio1 reference tokens, CSS at-rules (@import, @media, …), and tokens that start with a digit or contain ; & ( {.

Using Visual DNA in Generations

Pass visual_dna_ids (array of Visual DNA ids) to maintain character/product consistency. The generation routes that actually consume it are:

RouteConsumes visual_dna_ids
POST /v1/generate/imageYes
POST /v1/generate/image-editYes
POST /v1/generate/video/from-imageYes
POST /v1/generate/first-last-frameYes
POST /v1/generate/elementsYes
POST /v1/generate/creative-directorYes
POST /v1/generate/video (text-to-video)Yes
POST /v1/generate/video-from-videoYes

Other generation routes (music, speech, sound, lipsync, 3D, transcription) do not take visual_dna_ids.

On text-to-video, a DNA's stills ride as reference images only on models that accept reference images in that mode (elements_max_images > 0 on GET /api/v1/models; with several models the smallest cap applies), and Kling/WAN models receive the DNA natively as an element. On a text-to-video model with no reference-image slots, no stills are sent — the DNA is carried by name/description only — so for strict identity on such models, generate a still with POST /v1/generate/image + visual_dna_ids, then animate it with POST /v1/generate/video/from-image. On video-to-video, a DNA that fails to load (missing or no access) is dropped without failing the generation.

Image Generation

curl -X POST https://api.kolbo.ai/api/v1/generate/image \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "@emma standing in a coffee shop",
    "visual_dna_ids": ["6601a1b2c3d4e5f6a7b8c9d0"],
    "aspect_ratio": "1:1"
  }'

Video Generation (image-to-video)

curl -X POST https://api.kolbo.ai/api/v1/generate/video/from-image \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://cdn.kolbo.ai/....jpg",
    "prompt": "@emma walking through a park",
    "visual_dna_ids": ["6601a1b2c3d4e5f6a7b8c9d0"],
    "duration": 5
  }'

The number of Visual DNAs a model accepts is per-model. Call GET /api/v1/models and read max_visual_dna on the chosen model; supports_visual_dna is the authoritative boolean — a model can support DNA without exposing a numeric cap. On image generation the server attaches each DNA's reference images (its reference sheet first, when it has one). With prompt enhancement on, a synthesis pass weaves the extracted identity into the prompt; with enhance_prompt: false the prompt is not rewritten by an LLM — each @Name is only replaced by the @ImageN token of the slot its stills landed in, and the stills carry the identity. (A preset, moodboard or cinematic selection still forces enhancement on; a Visual DNA alone does not.) A DNA the caller has no access to is silently dropped rather than erroring.

JavaScript Example

const fs = require("fs");
const FormData = require("form-data");

const API_KEY = "kolbo_live_YOUR_API_KEY";
const BASE = "https://api.kolbo.ai/api";

async function main() {
  // 1. Create a Visual DNA from reference images
  const form = new FormData();
  form.append("name", "mascot");
  form.append("dnaType", "character");
  form.append("images", fs.createReadStream("mascot1.jpg"));
  form.append("images", fs.createReadStream("mascot2.jpg"));

  const createRes = await fetch(`${BASE}/v1/visual-dna`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, ...form.getHeaders() },
    body: form
  });
  const { visual_dna } = await createRes.json();
  console.log("Created Visual DNA:", visual_dna.id);

  // 2. Generate an image using the Visual DNA
  const genRes = await fetch(`${BASE}/v1/generate/image`, {
    method: "POST",
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      prompt: "@mascot presenting a new product",
      visual_dna_ids: [visual_dna.id]
    })
  });
  const { poll_url } = await genRes.json();

  // 3. Poll until the generation completes
  let result;
  do {
    await new Promise((r) => setTimeout(r, 3000));
    const status = await fetch(`${BASE}${poll_url}`, {
      headers: { "X-API-Key": API_KEY }
    });
    result = await status.json();
  } while (result.state === "processing");

  console.log("Image URLs:", result.result.urls);
}

main();

Python Example

import time
import requests

API_KEY = "kolbo_live_YOUR_API_KEY"
BASE = "https://api.kolbo.ai/api"
HEADERS = {"X-API-Key": API_KEY}

# 1. Create a Visual DNA from reference images
files = [
    ("images", ("mascot1.jpg", open("mascot1.jpg", "rb"), "image/jpeg")),
    ("images", ("mascot2.jpg", open("mascot2.jpg", "rb"), "image/jpeg")),
]
data = {"name": "mascot", "dnaType": "character"}

create_res = requests.post(f"{BASE}/v1/visual-dna", headers=HEADERS, data=data, files=files)
visual_dna = create_res.json()["visual_dna"]
print("Created Visual DNA:", visual_dna["id"])

# 2. Generate an image using the Visual DNA
gen_res = requests.post(
    f"{BASE}/v1/generate/image",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "prompt": "@mascot presenting a new product",
        "visual_dna_ids": [visual_dna["id"]],
    },
)
poll_url = gen_res.json()["poll_url"]

# 3. Poll until the generation completes
while True:
    time.sleep(3)
    status = requests.get(f"{BASE}{poll_url}", headers=HEADERS).json()
    if status["state"] != "processing":
        break

print("Image URLs:", status["result"]["urls"])

Tips

  • Upload up to 4 diverse reference image files (different angles, lighting) — that is the route's hard cap on file parts.
  • Generate a reference sheet and attach it via characterSheetUrl; it is the strongest consistency lever for every DNA type.
  • Visual DNA IDs work with both image and video generation endpoints.
  • Global Visual DNAs (provided by Kolbo) appear in your list but cannot be edited, deleted or filed into folders — import one (POST /v1/visual-dna/:id/import) to get an editable personal copy.