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.
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: 10 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. Rejected if it matches Image1, Image2, … (reserved for image references). If you already own a DNA with this name the server auto-appends (1), (2), … rather than failing. Any language/script is accepted. |
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). URL references are only honoured for mediaType: "image" — video/audio URLs are skipped with a warning; upload those as files. |
images (legacy) | array | No* | Legacy alias for media — 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.
There is no way to check this over the SDK — the resolved voice object is not returned by
POST, GET /v1/visual-dna, or GET /v1/visual-dna/:id. (kling_voice_id on the list/get
responses 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, and are only stored when dnaType is character. The MCP tool does not expose them.
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 "images=@photo1.jpg" \
-F "images=@photo2.jpg" \
-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",
"description": "AI-generated visual analysis...",
"thumbnail_url": "https://cdn.kolbo.ai/visual-dna/...",
"images": ["https://cdn.kolbo.ai/..."],
"created_at": "2026-03-06T12:00:00Z"
}
}Visual DNA creation is synchronous — the request stays open while the references are uploaded and analyzed. Success is HTTP 201.
Errors
Failures come back as { "success": false, "error": "…", "code": "…" }:
| Status | code | Meaning |
|---|---|---|
400 | VISUAL_DNA_ERROR | Name missing, over 100 chars, or matching Image<n>; or no reference supplied (no files and no usable media/images entries). Read error for the specific message. |
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). |
Every 400 on this route collapses to code: "VISUAL_DNA_ERROR". The in-app controller emits a
more specific INVALID_VISUAL_DNA_NAME code on the name check, but the SDK wrapper reads a
different key when building its envelope and that value never reaches you — branch on the error
message, not the code, for 400s.
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), or mine (personal + organization + shared-project, globals excluded). Omit for everything accessible. personal / global / organization are matched case-insensitively after trimming; mine and global additionally narrow the database query, but only on an exact lowercase match. |
search | string | Search by name, tags, or description (case-insensitive) |
collection | string | Filter global presets by collection: cast, influencers, props, locations, styles, glamour, street |
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 to the result, each flagged with an owner object and is_mine: false. View-only members and non-members get no extra items. |
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 applied as a post-filter: the server still loads
the full accessible set (including the global catalog) before narrowing the response, so they
reduce payload size but not query cost. Only the exact lowercase values global and mine change
what is read from the database — and scope=global alone (no search, collection, or tags)
is additionally served from a 15-minute cache, so it is the fastest way to browse the preset
catalog. scope=GLOBAL returns the same items but skips both the narrowed query and the cache.
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
}Field notes:
owner/is_mine— attribution. For DNAs surfaced throughproject_id,ownercarries the teammate'suserId,nameandavatar; for your own itemsnameandavatarare empty on the list endpoint.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". kling_voice_idis included when the DNA has a voice profile.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".
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, plus 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"
}
}The DNA's internal systemPrompt (the extracted identity prompt) is deliberately not returned
by any SDK endpoint.
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. Failures come back as success: false with code of FORBIDDEN (403), NOT_FOUND (404), or DELETE_ERROR.
Reference sheet (stronger consistency)
A 6-panel 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 the DNA.
POST /api/v1/visual-dna/character-sheetRate limit: 10 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. imageUrls is accepted as an alias. Each URL is SSRF-validated — private/internal hosts are rejected. |
sheet_type | string | No | character (turnaround) | product (angles + branding/material/construction close-ups) | environment (location angles + one signature detail) | style (the same look on six varied subjects). sheetType is accepted as an alias. An unrecognised value silently falls back to character rather than erroring. |
These are the only two fields the SDK route reads. It rebuilds the request body from scratch
before calling the in-app handler, so the projectId and presetId that the in-app route accepts
are dropped — a sheet generated through the SDK is never auto-attached to an existing DNA. Attach it
yourself with characterSheetUrl on POST /v1/visual-dna.
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 flat: { "success": false, "error": "…" }. The underlying
code, required, and available fields the in-app route emits on an insufficient-credits
403 are not forwarded by the SDK wrapper — branch on the HTTP status, not on a code
field. A generation failure comes back as 500 with the same flat shape.
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 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. Text-to-video (POST /v1/generate/video) never parses them — see the callout below.#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 the DNA's reference images and forces the
prompt-synthesis pass 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) | No — accepted and ignored |
POST /v1/generate/video-from-video | No — accepted and ignored |
POST /v1/generate/video and POST /v1/generate/video-from-video accept visual_dna_ids in the
request body without erroring, but the text-to-video and video-to-video controllers never read it
— the generation runs with no identity conditioning and no warning. For a DNA-consistent video,
generate a still with POST /v1/generate/image + visual_dna_ids, then animate it with
POST /v1/generate/video/from-image (which does consume the field).
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) and weaves the extracted
identity into the prompt through a synthesis pass. That pass is mandatory: attaching a Visual
DNA overrides enhance_prompt: false back to on, because a raw @mention means nothing to the
model. 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: "Brand 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": "Brand 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 deleted or filed into folders.