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-dnaRate limit: 30 requests/minute per user (shared SDK generation bucket).
Request Body (multipart/form-data, or JSON when every reference is a URL)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–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_id | string | No | Create 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. |
shareWithOrganization | boolean | No | true (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. |
dnaType | string | No | character | 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". |
promptHelper | string | No | Free-text description/notes that steer the DNA extraction. |
images | file(s) | No* | Up to 4 reference image files (multer maxCount: 4). |
videos | file | No* | Up to 1 reference video file. |
audio | file | No* | Up to 1 reference audio file. |
media | array or JSON string | No* | 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 string | No* | Legacy alias for media (used only when media is absent) — same object shape; items without mediaType default to image. |
characterSheetUrl | string | No | URL of a reference sheet (see Reference sheet) to set as this DNA's primary reference. Works for every dnaType. |
voiceSource | string | No | clone | 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). |
assignedVoiceId | string | No | Required 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": "…" }:
| Status | code | Meaning |
|---|---|---|
400 | INVALID_VISUAL_DNA_NAME | Name over 100 chars or matching Image<n>. |
400 | VISUAL_DNA_ERROR | Name missing, or no reference supplied (no files and no usable media/images entries). Read error for the specific message. |
403 | ACCESS_DENIED | project_id given, but you do not have edit access to that project. |
422 | ANALYSIS_FAILED | The vision model declined the reference images — retry with different references. |
500 | ANALYSIS_FAILED | The 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. |
500 | UPLOAD_FAILED | A reference file/URL could not be uploaded or transcoded. |
500 | DATABASE_ERROR | Save failed, e.g. an out-of-enum dnaType. |
500 | UNEXPECTED_ERROR | Anything 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-dnaQuery Parameters
| Parameter | Type | Description |
|---|---|---|
scope | string | Filter 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. |
search | string | Case-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. |
collection | string | Filter 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.) |
tags | string | Comma-separated tags to filter by (OR logic) |
project_id | string | A 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. |
page | integer | 1-based page. Paging is applied only when page or limit is sent; otherwise the full list is returned. |
limit | integer | Page 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:
countis the number of items in this response;totalis the number after scope/collection filtering and before paging;has_moreistruewhen a later page exists (alwaysfalsewithoutpage/limit).descriptionon the list endpoint is whitespace-collapsed and clipped to 400 characters;GET /v1/visual-dna/:idreturns the full text.imagesis 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_mineistrueonly for a non-global DNA you created; global presets and DNAs another user created come back withis_mine: falseandowner.userIdset to their creator;nameandavatarare filled only for the project owner's DNAs surfaced throughproject_id(other teammates' DNAs and your own items carry an emptynameandnullavataron the list endpoint — useGET /v1/visual-dna/:idfor the resolved owner).sheet_url— present for anydna_typewhen the DNA has a stored reference sheet (and the stored value is not an inlinedata: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 fordna_type: "character". - Voice fields:
voice,has_voice_referenceandsinging_voiceappear only when present (see the voice callout under Create).kling_voice_idis included when the Kling adapter registered a voice.is_sharedis included when the DNA has been shared via a public link.
Get Visual DNA
Endpoint
GET /api/v1/visual-dna/:idQuery Parameters
| Parameter | Type | Description |
|---|---|---|
project_id | string | Required 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/:idRate 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).
| Field | Notes |
|---|---|
name | New 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 / promptHelper | Description / 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 / dnaType | character / style / product / scene / environment. Changing type re-analyzes |
character_sheet_url / characterSheetUrl | New primary sheet URL. A changed sheet re-analyzes when references are also sent |
remove_character_sheet / removeCharacterSheet | true (or "true") clears the stored sheet. Without it, a DNA with no sheet falls back to its first still |
images (files) / videos / audio | Replacement 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 |
isActive | true restores a soft-deleted DNA (see Delete). Applied only on a metadata-only update — ignored when references or dnaType change in the same request |
systemPrompt | Write-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/importRate limit: 120 requests/minute per user (shared SDK media bucket).
| Field | Type | Required | Description |
|---|---|---|---|
project_id | string | For non-global sources | A 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/:idExample
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-sheetRate limit: 30 requests/minute per user (shared SDK generation bucket).
| Field | Type | Required | Description |
|---|---|---|---|
image_urls | array of string | Yes | Non-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_type | string | No | character (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/foldersReturns 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| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Folder name, trimmed. Unique per user — an exact duplicate returns 409. Empty/whitespace-only returns 400. |
color | string | No | Free-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| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | New name. Always required — pass the current name to keep it. Duplicate name → 409. |
color | string | No | New 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/:folderIdThe 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| Field | Type | Required | Description |
|---|---|---|---|
folder_id | string or null | Yes | Target 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 goPrompt 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:
@Nameresolves a Visual DNA by name. On text-to-video (POST /v1/generate/video) and image-to-video (POST /v1/generate/video/from-image),@Namementions are parsed into DNA ids only whenvisual_dna_idsis not supplied and at least one selected model reportssupports_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.#Nameresolves a moodboard by name. An explicitmoodboard_idin 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:
| Route | Consumes visual_dna_ids |
|---|---|
POST /v1/generate/image | Yes |
POST /v1/generate/image-edit | Yes |
POST /v1/generate/video/from-image | Yes |
POST /v1/generate/first-last-frame | Yes |
POST /v1/generate/elements | Yes |
POST /v1/generate/creative-director | Yes |
POST /v1/generate/video (text-to-video) | Yes |
POST /v1/generate/video-from-video | Yes |
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.