> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Characters

> Train a reusable on-screen persona from reference photos, then render images as it — the same face, hair and build across a whole channel.

> [!NOTE]
>
> A character is the media sibling of an [identity](https://vetta.sh/docs/api/identities.md): an identity is *who an agent is being* to the world (an inbox, a number); a character is *who it renders as* on screen.

Two renders of "a young woman with freckles" are two different women, so a channel of shorts has no recurring host. A **character** fixes that: it is trained once from a handful of reference stills and then pinned into every later [image](https://vetta.sh/docs/api/images.md), so the same face carries across every render — the AI-influencer primitive. Training takes minutes, so `POST /v1/characters` does not wait: it answers `202` with the character in `training`, and you read it back until it is `ready`. An agent does the same with the [`create_character`](https://vetta.sh/docs/capabilities/tools#characters) family of tools; a character made here is one a session can render as, and the other way round.

## The character object

**id** `string`

`chr_…`.

**object** `string`

Always `character`.

**name** `string`

How a person and an agent address it, up to 100 characters. Unique among the organization's live characters, ignoring case.

**description** `string | null`

Who the character is — looks, age, vibe, wardrobe — up to 1,024 characters. Read by the model when it renders as them.

**status** `string`

`training` → `ready` | `failed`. `ready` is the only state a render can pin.

**source\_images** `string[]`

The reference stills it was trained from, **as you gave them**: a `fil_` id or a public URL. Never the short-lived signed link the trainer was handed.

**thumbnail\_url** `string | null`

Reserved for a trained preview. Always `null` today: render a still with the character to see it.

**reason** `string | null`

Why a `failed` training failed; `null` otherwise.

**scope** `string`

`organization` — your own, trained from your photos — or `platform`: a **prebuilt** character every organization can render as, ready with no training. A prebuilt character cannot be deleted.

**persona** `object | null`

Who the character is, as up to five lines (`niche`, `bio`, `voice`, `wardrobe`, `hooks`, each up to 500 characters), read by your agents when they write as them. `null` when none was given.

**style\_pack** `string[]`

The [looks](https://vetta.sh/docs/api/images.md#looks) the character is usually drawn in, by name. A trained character given none gets Phone snap, Realistic and Everyday.

**sheet** `string[]`

A prebuilt character's sample stills, as paths on the dashboard. Empty for your own.

**metadata** `object`

Arbitrary key/value pairs. A character you trained carries `consent` here: `attested`, `by` (the member or API key that confirmed it, or the session whose approved call trained it), `at` and the `statement` that was confirmed.

**created\_at** `string`

Creation timestamp.

**updated\_at** `string`

When its status or fields last changed.

## Create a character

`POST /v1/characters` → `202 Accepted` — scope `agents:write`, [`Idempotency-Key`](https://vetta.sh/docs/api/overview#idempotency) **required**.

> [!WARNING]
>
> Train only your own face, or the face of someone who has agreed to an AI likeness of them. Every training records who confirmed that and when, and photos of a recognizable public figure are refused. Using someone's likeness without their permission breaks the [Terms](https://usenaive.ai/terms).

**name** `string` *required*

A short, unique name, such as `Ava`. 1–100 characters. A name a live character already holds is `validation_failed` before anything is trained or billed.

**description** `string`

Who the character is. Up to 1,024 characters. Left out, it is a one-line description of how the person in the photos looks (apparent gender, age, hair), read from the photos when they are checked: every render as the character uses it to keep the person the same.

**persona** `object`

Who they are: any of `niche`, `bio`, `voice`, `wardrobe`, `hooks`, each up to 500 characters.

**style\_pack** `string[]`

Up to 12 [look](https://vetta.sh/docs/api/images.md#looks) names they are usually drawn in. An unknown look is `validation_failed`. Omitted or empty, they are Phone snap, Realistic and Everyday.

**consent** `object` *required*

`{ "attested": true }` — you confirm: *"I confirm the person in these photos is me, or has given me permission to create an AI likeness of them, and is not a public figure."* Anything else is `validation_failed` before anything is checked, trained or billed. Kept on the character as `metadata.consent`, with who confirmed it and when. An agent's `create_character` always waits for a person to approve the call, whatever its tool policy says: approving it is that person's confirmation of this statement.

**image\_urls** `string[]` *required*

One to **sixteen** reference photos of the *same* person, each a stored file's `fil_` id (an upload, or an image a [render](https://vetta.sh/docs/api/images.md) filed) or a public `http(s)` URL. More, varied angles train a stronger likeness. A `fil_` this organization does not hold is `not_found`; one that is not an image, a private or loopback host, and a `data:` or `file:` URL are `validation_failed`. The first two photos are checked for a recognizable public figure before anything is billed; if they show one, the request is `validation_failed` on `image_urls` with the reason.

```bash cURL theme={"system"}
curl -fsSL https://api.vetta.sh/v1/characters \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{
    "name": "Ava",
    "description": "Late twenties, freckles, copper bob, oversized denim jacket",
    "image_urls": ["fil_01J9A...", "fil_01J9B...", "https://example.com/ava-profile.jpg"],
    "persona": { "niche": "Fashion · fit checks", "voice": "Dry, quick, never salesy" },
    "style_pack": ["Phone snap", "Quiet luxury"],
    "consent": { "attested": true }
  }'
```

```ts SDK theme={"system"}
const ava = await client.characters.create({
  name: "Ava",
  description: "Late twenties, freckles, copper bob, oversized denim jacket",
  image_urls: ["fil_01J9A...", "fil_01J9B...", "https://example.com/ava-profile.jpg"],
  persona: { niche: "Fashion · fit checks", voice: "Dry, quick, never salesy" },
  style_pack: ["Phone snap", "Quiet luxury"],
  consent: { attested: true },
});
```

```bash CLI theme={"system"}
vetta character create --name Ava --consent --image fil_01J9A... --image fil_01J9B... --image https://example.com/ava-profile.jpg \
  --persona '{"niche":"Fashion · fit checks","voice":"Dry, quick, never salesy"}' --style-pack "Phone snap" --style-pack "Quiet luxury"
```

```json 202 Accepted theme={"system"}
{
  "id": "chr_01J9CD...",
  "object": "character",
  "name": "Ava",
  "description": "Late twenties, freckles, copper bob, oversized denim jacket",
  "status": "training",
  "source_images": ["fil_01J9A...", "fil_01J9B...", "https://example.com/ava-profile.jpg"],
  "thumbnail_url": null,
  "reason": null,
  "scope": "organization",
  "persona": { "niche": "Fashion · fit checks", "voice": "Dry, quick, never salesy" },
  "style_pack": ["Phone snap", "Quiet luxury"],
  "sheet": [],
  "metadata": {
    "consent": {
      "attested": true,
      "by": "usr_01J8ZX...",
      "at": "2026-10-15T12:00:00.000Z",
      "statement": "I confirm the person in these photos is me, or has given me permission to create an AI likeness of them, and is not a public figure."
    }
  },
  "created_at": "2026-10-15T12:00:00.000Z",
  "updated_at": "2026-10-15T12:00:00.000Z"
}
```

A replay under the same `Idempotency-Key` answers the character the first request made — it is not trained or billed twice. Once that character is deleted, a replay of its key is `409 idempotency_conflict`; send a new key to train another.

## Retrieve a character

`GET /v1/characters/{id}` → `200 OK` — scope `agents:read`. `{id}` is the `chr_` id **or** the name; an exact id wins over a name. Poll it after a create: each read advances the status from the trainer, so `training` turns into `ready` or `failed` on the read that sees it. A training takes a few minutes.

## List characters

`GET /v1/characters` → `200 OK`, [cursor-paginated](https://vetta.sh/docs/api/pagination.md), newest first — scope `agents:read`. `{ data: Character[], has_more, next_cursor }`.

Your own characters come first, then the prebuilt ones.

**status** `string`

`training`, `ready` or `failed`.

**scope** `string`

`organization` for your own only, `platform` for the prebuilt ones only.

**limit** `number`

Page size, 1–100.

**after** `string`

The `next_cursor` of the previous page.

## Update a character

`PATCH /v1/characters/{id}` changes who a character is, by `chr_` id or name: `name`, `description`, `persona` and `style_pack`. A `persona` is merged line by line, so lines you leave out stay. The face can't change: that's a new training. A prebuilt character is `409 state_conflict`, and a name another character holds is `validation_failed`.

```bash cURL theme={"system"}
curl -fsSL -X PATCH https://api.vetta.sh/v1/characters/Ava \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "persona": { "wardrobe": "Linen shirts and old denim" } }'
```

## Delete a character

`DELETE /v1/characters/{id}` → `200 OK` — scope `agents:write`. `{id}` is the `chr_` id or the name. A soft delete: the character can no longer be rendered as and its name is free for a new one, while every file it rendered keeps its `source.character_id`. A prebuilt character (`scope: "platform"`) cannot be deleted: `409 state_conflict`.

```json Response theme={"system"}
{ "id": "chr_01J9CD...", "object": "character", "deleted": true }
```

## Rendering as a character

Pass `character` — the `chr_` id or name of a `ready` character — to [`POST /v1/media/images`](https://vetta.sh/docs/api/images#generate-images). The render is routed to a model that pins one, and the same face carries across every image. Add `style` to draw it in a [look](https://vetta.sh/docs/api/images.md#looks).

To keep a scene's room, props and outfit across shots, pass `character` together with `image_urls` naming a still of that character: the render is an edit of that still in a new pose or framing, and it is a still of the character too, so it can be the first frame of the next video.

A character carries into video through the **still** it renders: generate an image as the character, then pass `character` and the still's `fil_` id as the `image_urls` first frame of [`POST /v1/media/videos`](https://vetta.sh/docs/api/video#generate-a-video). The first frame must be a still rendered as that character, or the request is `validation_failed`, and only a model that animates a still is used.

```ts theme={"system"}
const still = await client.media.images.generate({ prompt: "Ava waves at the camera on a rooftop at golden hour", character: "Ava", style: "Phone snap" });
const job = await client.media.videos.generate({ prompt: "she waves, the camera pushes in slowly", character: "Ava", image_urls: [still.files[0].id] });
```

A post that attaches a file rendered as a character is not labelled AI-generated on its own: the label is opt-in, so set `ai_generated: true` on the post to add it. See [AI-generated posts](https://vetta.sh/docs/api/social.md#ai-generated-posts).

## Billing

A training costs dollars, not cents, so unlike a render it is **admitted first**: the organization's prepaid balance must cover one training, else `402 insufficient_credits` before anything is bought. It is then billed once, on the media tier, when the trainer accepts the photos — under the character the request's idempotency key names, so a replay charges nothing. A request refused before training (a taken name, no consent, a public figure, an unreadable photo) is not billed. A training that later ends `failed` is refunded — credited back to the organization on the read that sees it fail. Reads, lists and deletes are free. See [Pricing](https://vetta.sh/docs/platform/pricing.md#media-generation--per-finished-job-media-component).

## Errors

| Code | When |
| - | - |
| `validation_failed` | Missing `Idempotency-Key`; no `consent` or `consent.attested` not `true`; photos that show a recognizable public figure (`image_urls`, with the reason); `name` empty, over 100 characters or already held by a live character; zero or more than sixteen `image_urls`; a stored file that is not an image; a private-host, `data:` or `file:` URL. On a render: a `character` still `training` or `failed`, or with `image_urls` that are not all stills of it. |
| `insufficient_credits` | `402`: the prepaid balance cannot cover a training. Nothing is trained or billed. |
| `not_found` | No such `chr_` id or name in this organization, or it was deleted; an `image_urls` `fil_` this organization does not hold. |
| `provider_error` | The trainer refused the photos, or could not be reached. On a create nothing is created or billed; on a read the character is answered as it stood once the trainer answers again. |
| `idempotency_conflict` | `409`: the `Idempotency-Key` already created a character that has since been deleted. |
| `feature_not_configured` | This deploy has no media backend that trains characters. |

> [card] [In a session](https://vetta.sh/docs/capabilities/tools.md#characters)
>
> `create_character`, `get_character`, `list_characters` and `delete_character` — the same lifecycle as agent tools.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.