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.

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: 10 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. 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.
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). URL references are only honoured for mediaType: "image" — video/audio URLs are skipped with a warning; upload those as files.
images (legacy)arrayNo*Legacy alias for media — 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. 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": "…" }:

StatuscodeMeaning
400VISUAL_DNA_ERRORName 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.
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).

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-dna

Query Parameters

ParameterTypeDescription
scopestringFilter 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.
searchstringSearch by name, tags, or description (case-insensitive)
collectionstringFilter global presets by collection: cast, influencers, props, locations, styles, glamour, street
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 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 through project_id, owner carries the teammate's userId, name and avatar; for your own items name and avatar are empty on the list endpoint.
  • 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".
  • kling_voice_id is included when the DNA has a voice profile. 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".

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/: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. 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-sheet

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

FieldTypeRequiredDescription
image_urlsarray of stringYesNon-empty array of reference image URLs. imageUrls is accepted as an alias. Each URL is SSRF-validated — private/internal hosts are rejected.
sheet_typestringNocharacter (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_sheetcreate_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 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. Text-to-video (POST /v1/generate/video) never parses them — see the callout below.
  • #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 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:

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)No — accepted and ignored
POST /v1/generate/video-from-videoNo — 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.